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
@@ -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
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The `Fluxbit::SkeletonComponent` is a customizable skeleton loading component that extends `Fluxbit::Component`.
5
+ # It provides animated placeholders for content that is loading, supporting various types like text, images, cards,
6
+ # avatars, and more complex layouts.
7
+ #
8
+ # @example Basic usage
9
+ # = fx_skeleton
10
+ # = fx_skeleton(variant: :image)
11
+ # = fx_skeleton(variant: :card)
12
+ #
13
+ # @see docs/02_Components/Skeleton.md For detailed documentation.
14
+ class Fluxbit::SkeletonComponent < Fluxbit::Component
15
+ include Fluxbit::Config::SkeletonComponent
16
+
17
+ ##
18
+ # Initializes the skeleton component with the given properties.
19
+ #
20
+ # @param [Hash] **props The properties to customize the skeleton.
21
+ # @option props [Symbol] :variant (:default) The type of skeleton (:default, :text, :image, :video, :avatar, :card, :widget, :list, :testimonial, :button).
22
+ # @option props [Boolean] :animation (true) Whether to show the pulse animation.
23
+ # @option props [Integer] :rows (3) Number of text rows for default/text variants.
24
+ # @option props [Symbol] :size (:medium) Size for avatar, image, and video variants (:small, :medium, :large).
25
+ # @option props [Integer] :lines (nil) Number of lines for text-based variants.
26
+ # @option props [String] :remove_class ('') CSS classes to remove from the default class list.
27
+ # @option props [Hash] **props Remaining options declared as HTML attributes.
28
+ #
29
+ # @return [Fluxbit::SkeletonComponent]
30
+ def initialize(**props)
31
+ super
32
+ @props = props
33
+
34
+ @variant = options(@props.delete(:variant),
35
+ collection: [ :default, :text, :image, :video, :avatar, :card, :widget, :list, :testimonial, :button ],
36
+ default: @@variant)
37
+ @animation = options(@props.delete(:animation), default: @@animation)
38
+ @rows = options(@props.delete(:rows), default: @@rows).to_i
39
+ @size = options(@props.delete(:size), collection: [ :small, :medium, :large ], default: :medium)
40
+ @lines = @props.delete(:lines)&.to_i
41
+
42
+ declare_classes
43
+ @props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
44
+ @props[:role] = "status"
45
+ @props["aria-label"] = "Loading"
46
+ end
47
+
48
+ def call
49
+ tag.div(**@props) do
50
+ case @variant
51
+ when :default, :text
52
+ render_text_skeleton
53
+ when :image
54
+ render_image_skeleton
55
+ when :video
56
+ render_video_skeleton
57
+ when :avatar
58
+ render_avatar_skeleton
59
+ when :card
60
+ render_card_skeleton
61
+ when :widget
62
+ render_widget_skeleton
63
+ when :list
64
+ render_list_skeleton
65
+ when :testimonial
66
+ render_testimonial_skeleton
67
+ when :button
68
+ render_button_skeleton
69
+ end
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def declare_classes
76
+ base_classes = []
77
+ base_classes << styles[:base] if @animation
78
+ base_classes << styles[:container]
79
+
80
+ add(class: base_classes.join(" "), to: @props, first_element: true)
81
+ end
82
+
83
+ def render_text_skeleton
84
+ lines_count = @lines || @rows
85
+ content = []
86
+
87
+ lines_count.times do |index|
88
+ width_class = case index
89
+ when 0 then styles[:widths][:first_line]
90
+ when lines_count - 1 then styles[:widths][:last_line]
91
+ else styles[:widths][:full]
92
+ end
93
+
94
+ content << tag.div(class: "#{styles[:text][:line]} #{width_class} #{styles[:spacing][:small]}")
95
+ end
96
+
97
+ content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
98
+ safe_join(content)
99
+ end
100
+
101
+ def render_image_skeleton
102
+ content = []
103
+ content << tag.div(class: "#{styles[:image][:container]} #{styles[:image][@size]}") do
104
+ concat(
105
+ tag.svg(class: styles[:image][:icon],
106
+ "aria-hidden": "true",
107
+ xmlns: "http://www.w3.org/2000/svg",
108
+ fill: "currentColor",
109
+ viewBox: "0 0 20 18") do
110
+ concat(
111
+ tag.path(d: "M18 0H2a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2Zm-5.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm4.376 10.481A1 1 0 0 1 16 15H4a1 1 0 0 1-.895-1.447l3.5-7A1 1 0 0 1 7.468 6a.965.965 0 0 1 .9.5l2.775 4.757 1.546-1.887a1 1 0 0 1 1.618.1l2.541 4a1 1 0 0 1 .028 1.011Z")
112
+ )
113
+ end
114
+ )
115
+ end
116
+ content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
117
+ safe_join(content)
118
+ end
119
+
120
+ def render_video_skeleton
121
+ content = []
122
+ content << tag.div(class: "#{styles[:video][:container]} #{styles[:video][@size]}") do
123
+ concat(
124
+ tag.svg(class: styles[:video][:icon],
125
+ "aria-hidden": "true",
126
+ xmlns: "http://www.w3.org/2000/svg",
127
+ fill: "currentColor",
128
+ viewBox: "0 0 16 20") do
129
+ concat(
130
+ tag.path(d: "M5 5V.13a2.96 2.96 0 0 0-1.293.749L.879 3.707A2.98 2.98 0 0 0 .13 5H5Z")
131
+ )
132
+ concat(
133
+ tag.path(d: "M14.066 0H7v5a2 2 0 0 1-2 2H0v11a1.97 1.97 0 0 0 1.934 2h12.132A1.97 1.97 0 0 0 16 18V2a1.97 1.97 0 0 0-1.934-2ZM9 13a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2Zm4 .382a1 1 0 0 1-1.447.894L10 13v-2l1.553-1.276a1 1 0 0 1 1.447.894v2.764Z")
134
+ )
135
+ end
136
+ )
137
+ end
138
+ content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
139
+ safe_join(content)
140
+ end
141
+
142
+ def render_avatar_skeleton
143
+ content = []
144
+ content << tag.div(class: styles[:avatar][@size])
145
+ content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
146
+ safe_join(content)
147
+ end
148
+
149
+ def render_card_skeleton
150
+ tag.div(class: styles[:card][:container]) do
151
+ content = []
152
+ content << tag.div(class: "#{styles[:card][:header]} #{styles[:spacing][:medium]} #{styles[:widths][:card_header]}")
153
+
154
+ lines_count = @lines || @rows
155
+ lines_count.times do |index|
156
+ width_class = index == lines_count - 1 ? styles[:widths][:last_line] : styles[:widths][:full]
157
+ spacing_class = index < lines_count - 1 ? styles[:spacing][:small] : ""
158
+ content << tag.div(class: "#{styles[:card][:body]} #{spacing_class} #{width_class}")
159
+ end
160
+
161
+ content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
162
+ safe_join(content)
163
+ end
164
+ end
165
+
166
+ def render_widget_skeleton
167
+ tag.div(class: styles[:widget][:container]) do
168
+ content = []
169
+ content << tag.div(class: "#{styles[:widget][:title]} #{styles[:widths][:widget_title]}")
170
+
171
+ lines_count = @lines || @rows
172
+ lines_count.times do |index|
173
+ width_class = index == lines_count - 1 ? styles[:widths][:last_line] : styles[:widths][:full]
174
+ spacing_class = index < lines_count - 1 ? styles[:spacing][:small] : ""
175
+ content << tag.div(class: "#{styles[:widget][:content]} #{spacing_class} #{width_class}")
176
+ end
177
+
178
+ content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
179
+ safe_join(content)
180
+ end
181
+ end
182
+
183
+ def render_list_skeleton
184
+ tag.div(class: styles[:list][:container]) do
185
+ content = []
186
+
187
+ lines_count = @lines || @rows
188
+ lines_count.times do
189
+ content << tag.div(class: styles[:list][:item]) do
190
+ concat(tag.div(class: styles[:list][:avatar]))
191
+ concat(
192
+ tag.div(class: styles[:list][:content]) do
193
+ concat(tag.div(class: "#{styles[:text][:line]} #{styles[:widths][:list_name]} #{styles[:spacing][:small]}"))
194
+ concat(tag.div(class: "#{styles[:text][:small]} #{styles[:widths][:list_content]}"))
195
+ end
196
+ )
197
+ end
198
+ end
199
+
200
+ content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
201
+ safe_join(content)
202
+ end
203
+ end
204
+
205
+ def render_testimonial_skeleton
206
+ tag.div(class: styles[:testimonial][:container]) do
207
+ content = []
208
+
209
+ lines_count = @lines || @rows
210
+ lines_count.times do |index|
211
+ width = index == lines_count - 1 ? styles[:widths][:testimonial_last] : styles[:widths][:full]
212
+ spacing_class = index < lines_count - 1 ? styles[:spacing][:small] : ""
213
+ content << tag.div(class: "#{styles[:testimonial][:quote]} #{width} #{spacing_class}")
214
+ end
215
+
216
+ content << tag.div(class: styles[:testimonial][:author]) do
217
+ concat(tag.div(class: styles[:testimonial][:avatar]))
218
+ concat(
219
+ tag.div do
220
+ concat(tag.div(class: "#{styles[:text][:line]} #{styles[:widths][:testimonial_name]} #{styles[:spacing][:small]}"))
221
+ concat(tag.div(class: "#{styles[:text][:small]} #{styles[:widths][:testimonial_title]}"))
222
+ end
223
+ )
224
+ end
225
+
226
+ content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
227
+ safe_join(content)
228
+ end
229
+ end
230
+
231
+ def render_button_skeleton
232
+ content = []
233
+ content << tag.div(class: styles[:button])
234
+ content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
235
+ safe_join(content)
236
+ end
237
+ end
@@ -0,0 +1,30 @@
1
+ <% if @text_outside && @text %>
2
+ <div class="<%= styles[:action][:text][:base] %>">
3
+ <span class="<%= text_classes %>"><%= @text %></span>
4
+ <% if @href %>
5
+ <%= link_to(@href, **@props) do %>
6
+ <%= icon @icon, class: icon_classes if @icon %>
7
+ <%= content if content.present? %>
8
+ <% end %>
9
+ <% else %>
10
+ <%= tag.button(**@props) do %>
11
+ <%= icon @icon, class: icon_classes if @icon %>
12
+ <%= content if content.present? %>
13
+ <% end %>
14
+ <% end %>
15
+ </div>
16
+ <% else %>
17
+ <% if @href %>
18
+ <%= link_to(@href, **@props) do %>
19
+ <%= icon @icon, class: icon_classes if @icon %>
20
+ <%= content if content.present? %>
21
+ <% end %>
22
+ <% else %>
23
+ <%= tag.button(**@props) do %>
24
+ <%= icon @icon, class: icon_classes if @icon %>
25
+ <%= content if content.present? %>
26
+ <% end %>
27
+ <% end %>
28
+ <% end %>
29
+
30
+ <%= render_popover_or_tooltip %>
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The `Fluxbit::SpeedDialActionComponent` is a component for rendering individual action items in a Speed Dial.
4
+ class Fluxbit::SpeedDialActionComponent < Fluxbit::Component
5
+ include Fluxbit::Config::SpeedDialComponent
6
+
7
+ # Initializes the SpeedDialAction component.
8
+ #
9
+ # @param [Hash] **props The properties to customize the speed dial action.
10
+ # @option props [String, Symbol] :icon (nil) The icon to display in the action button.
11
+ # @option props [String] :text (nil) The text label for the action.
12
+ # @option props [String] :tooltip (nil) The tooltip text (defaults to text if not provided).
13
+ # @option props [String] :href (nil) The URL to link to (creates an anchor tag instead of button).
14
+ # @option props [Boolean] :text_outside (false) Whether to display text outside the button.
15
+ # @option props [Boolean] :square (false) Whether to use square shape instead of rounded.
16
+ # @option props [Symbol] :tooltip_placement (:left) Tooltip placement.
17
+ # @option props [String] :remove_class ('') CSS classes to remove from the default class list.
18
+ # @option props [Hash] **props Remaining options declared as HTML attributes.
19
+ #
20
+ # @return [Fluxbit::SpeedDialActionComponent]
21
+ def initialize(**props)
22
+ super(**props.slice(:tooltip_text, :tooltip_placement, :tooltip_trigger))
23
+ @props = props
24
+
25
+ @icon = @props.delete(:icon)
26
+ @text = @props.delete(:text)
27
+ @tooltip = @props.delete(:tooltip) || @text
28
+ @href = @props.delete(:href)
29
+ @text_outside = options @props.delete(:text_outside), default: false
30
+ @square = options @props.delete(:square), default: false
31
+ @tooltip_placement = options @props.delete(:tooltip_placement), default: :left
32
+ @props[:id] ||= "speed-dial-action-#{random_id}"
33
+
34
+ # Set tooltip via parent component if provided
35
+ @tooltip_text = @tooltip if @tooltip.present?
36
+
37
+ add class: [
38
+ styles[:action][:base],
39
+ @square ? styles[:action][:shapes][:square] : styles[:action][:shapes][:rounded]
40
+ ], to: @props
41
+ @props[:type] ||= "button" unless @href
42
+
43
+ remove_class_from_props(@props)
44
+ end
45
+
46
+ def before_render
47
+ add_popover_or_tooltip
48
+ end
49
+
50
+ private
51
+
52
+ def icon_classes
53
+ styles[:action][:icon]
54
+ end
55
+
56
+ def text_classes
57
+ styles[:action][:text][:outside]
58
+ end
59
+ end
@@ -0,0 +1,33 @@
1
+ <div <%= tag.attributes(**@props) %> data-dial-init>
2
+ <% if top_position? %>
3
+ <button type="button" data-dial-toggle="<%= menu_id %>" aria-controls="<%= menu_id %>" aria-expanded="false" class="<%= classes trigger_classes %>">
4
+ <% if @trigger_icon.nil? %>
5
+ <%= plus_icon(class: styles[:trigger][:icon]) %>
6
+ <% else %>
7
+ <%= icon @trigger_icon, class: styles[:trigger][:icon] %>
8
+ <% end %>
9
+ <span class="<%= styles[:screen_reader] %>"><%= t("fluxbit.speed_dial.open_actions_menu") %></span>
10
+ </button>
11
+
12
+ <div id="<%= menu_id %>" class="<%= classes menu_classes %>">
13
+ <% actions.each do |action| %>
14
+ <%= action %>
15
+ <% end %>
16
+ </div>
17
+ <% else %>
18
+ <div id="<%= menu_id %>" class="<%= classes menu_classes %>">
19
+ <% actions.each do |action| %>
20
+ <%= action %>
21
+ <% end %>
22
+ </div>
23
+
24
+ <button type="button" data-dial-toggle="<%= menu_id %>" aria-controls="<%= menu_id %>" aria-expanded="false" class="<%= classes trigger_classes %>">
25
+ <% if @trigger_icon.nil? %>
26
+ <%= plus_icon(class: styles[:trigger][:icon]) %>
27
+ <% else %>
28
+ <%= icon @trigger_icon, class: styles[:trigger][:icon] %>
29
+ <% end %>
30
+ <span class="<%= styles[:screen_reader] %>"><%= t("fluxbit.speed_dial.open_actions_menu") %></span>
31
+ </button>
32
+ <% end %>
33
+ </div>