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,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The `Fluxbit::StepperComponent` is a customizable stepper component that extends `Fluxbit::Component`.
5
+ # It provides a visual representation of a multi-step process, supporting both horizontal and vertical orientations
6
+ # with various styling options including different colors and states.
7
+ #
8
+ # @example Basic usage
9
+ # = fx_stepper do |stepper|
10
+ # = stepper.with_step(title: "Step 1", state: :completed)
11
+ # = stepper.with_step(title: "Step 2", state: :active)
12
+ # = stepper.with_step(title: "Step 3")
13
+ # end
14
+ #
15
+ # @see docs/02_Components/Stepper.md For detailed documentation.
16
+ class Fluxbit::StepperComponent < Fluxbit::Component
17
+ include Fluxbit::Config::StepperComponent
18
+
19
+ renders_many :steps, lambda { |**attrs, &block|
20
+ step = Step.new(**attrs)
21
+ step.with_content(block.call) if block_given?
22
+ step
23
+ }
24
+
25
+ ##
26
+ # Initializes the stepper component with the given properties.
27
+ #
28
+ # @param [Hash] **props The properties to customize the stepper.
29
+ # @option props [Symbol] :orientation (:horizontal) The orientation of the stepper (:horizontal, :vertical).
30
+ # @option props [Symbol] :variant (:default) The variant of the stepper (:default, :progress, :detailed).
31
+ # @option props [Symbol] :color (:blue) The color theme of the active step (:blue, :green, :red, :yellow, :indigo, :purple).
32
+ # @option props [String] :remove_class ('') CSS classes to remove from the default class list.
33
+ # @option props [Hash] **props Remaining options declared as HTML attributes.
34
+ #
35
+ # @return [Fluxbit::StepperComponent]
36
+ def initialize(**props)
37
+ super
38
+ @props = props
39
+
40
+ @orientation = options @props.delete(:orientation), collection: styles[:base].keys, default: @@orientation
41
+ @variant = options @props.delete(:variant), collection: styles[:list].keys, default: @@variant
42
+ @color = options @props.delete(:color), collection: styles[:step][:default][:active].keys, default: @@color
43
+
44
+ add class: styles[:base][@orientation], to: @props, first_element: true
45
+ remove_class_from_props(@props)
46
+ end
47
+
48
+ def call
49
+ tag.div(**@props) do
50
+ tag.ol(class: styles[:list][@variant][@orientation]) do
51
+ safe_join(steps.map.with_index { |step, index| render_step(step, index) })
52
+ end
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def render_step(step, index)
59
+ last_step = index == steps.count - 1
60
+
61
+ content_tag(:li, class: step_item_classes) do
62
+ if @orientation == :vertical && @variant != :detailed
63
+ # Vertical layout: wrapper div for proper positioning
64
+ concat(
65
+ tag.div(class: styles[:layout][:wrapper]) do
66
+ content = []
67
+ content << tag.div(class: styles[:layout][:flex_center]) do
68
+ concat(render_step_indicator(step))
69
+ concat(render_step_content(step)) if step.title.present? || step.description.present? || step.content.present?
70
+ end
71
+ content << render_vertical_connector(step) unless last_step
72
+ safe_join(content)
73
+ end
74
+ )
75
+ elsif @orientation == :vertical && @variant == :detailed
76
+ # Detailed vertical: no connectors, just cards
77
+ concat(render_step_indicator(step))
78
+ concat(render_step_content(step)) if step.title.present? || step.description.present? || step.content.present?
79
+ else
80
+ # Horizontal layout: standard flow with connectors
81
+ concat(render_step_indicator(step))
82
+ concat(render_step_content(step)) if step.title.present? || step.description.present? || step.content.present?
83
+ concat(render_connector(step)) unless last_step
84
+ end
85
+ end
86
+ end
87
+
88
+ def step_item_classes
89
+ styles[:item][@variant][@orientation]
90
+ end
91
+
92
+ def render_step_indicator(step)
93
+ tag.div(class: step_circle_classes(step)) do
94
+ if step.icon.present?
95
+ anyicon(step.icon, class: styles[:step_icon][@variant][:completed])
96
+ elsif step.state == :completed && @variant == :progress
97
+ content_tag(:span, "✓", class: step_number_classes(step))
98
+ else
99
+ content_tag(:span, step.number || "", class: step_number_classes(step))
100
+ end
101
+ end
102
+ end
103
+
104
+ def render_step_content(step)
105
+ return "" unless step.title.present? || step.description.present? || step.content.present?
106
+
107
+ tag.div(class: styles[:content][@variant][@orientation]) do
108
+ content = []
109
+
110
+ content << tag.h3(step.title, class: step_title_classes(step)) if step.title.present?
111
+ content << tag.p(step.description, class: styles[:description]) if step.description.present?
112
+ content << step.content if step.content.present?
113
+
114
+ safe_join(content)
115
+ end
116
+ end
117
+
118
+ def render_connector(step)
119
+ return "" if @orientation == :horizontal && steps.count <= 1
120
+
121
+ connector_classes = if step.state == :completed
122
+ styles[:connector][:completed][@variant][@orientation]
123
+ elsif step.state == :active
124
+ styles[:connector][:active][@color][@variant][@orientation]
125
+ else
126
+ styles[:connector][@variant][@orientation]
127
+ end
128
+
129
+ tag.div(class: connector_classes)
130
+ end
131
+
132
+ def render_vertical_connector(step)
133
+ # Position based on variant (different circle sizes)
134
+ left_position = styles[:vertical_connector][:positions][@variant] || styles[:vertical_connector][:positions][:default]
135
+
136
+ connector_classes = [
137
+ styles[:vertical_connector][:base],
138
+ left_position,
139
+ vertical_connector_color(step)
140
+ ].compact.join(" ")
141
+
142
+ tag.div(class: connector_classes)
143
+ end
144
+
145
+ def vertical_connector_color(step)
146
+ case step.state
147
+ when :completed
148
+ styles[:vertical_connector][:colors][:completed]
149
+ when :active
150
+ styles[:vertical_connector][:colors][@color] || styles[:vertical_connector][:colors][:blue]
151
+ else
152
+ styles[:vertical_connector][:colors][:default]
153
+ end
154
+ end
155
+
156
+ def step_circle_classes(step)
157
+ case step.state
158
+ when :completed
159
+ styles[:step][@variant][:completed]
160
+ when :active
161
+ styles[:step][@variant][:active][@color]
162
+ else
163
+ styles[:step][@variant][:base]
164
+ end
165
+ end
166
+
167
+ def step_number_classes(step)
168
+ case step.state
169
+ when :completed
170
+ styles[:step_number][@variant][:completed]
171
+ when :active
172
+ styles[:step_number][@variant][:active][@color]
173
+ else
174
+ styles[:step_number][@variant][:base]
175
+ end
176
+ end
177
+
178
+ def step_title_classes(step)
179
+ case step.state
180
+ when :completed
181
+ styles[:title][:completed]
182
+ when :active
183
+ styles[:title][:active][@color]
184
+ else
185
+ styles[:title][:base]
186
+ end
187
+ end
188
+
189
+ # Nested step component
190
+ class Step < Fluxbit::Component
191
+ include Fluxbit::Config::StepperComponent
192
+
193
+ attr_reader :title, :description, :state, :number, :icon
194
+
195
+ ##
196
+ # Initializes a step item for the stepper.
197
+ #
198
+ # @param [Hash] **props The properties to customize the step.
199
+ # @option props [String] :title The title of the step.
200
+ # @option props [String] :description The description of the step.
201
+ # @option props [Symbol] :state (:pending) The state of the step (:pending, :active, :completed).
202
+ # @option props [String, Integer] :number The step number or custom text.
203
+ # @option props [String] :icon The icon name to display for completed state.
204
+ #
205
+ # @return [Fluxbit::StepperComponent::Step]
206
+ def initialize(**props)
207
+ super
208
+ @props = props
209
+
210
+ @title = @props.delete(:title)
211
+ @description = @props.delete(:description)
212
+ @state = options @props.delete(:state), collection: [ :pending, :active, :completed ], default: :pending
213
+ @number = @props.delete(:number)
214
+ @icon = @props.delete(:icon)
215
+
216
+ remove_class_from_props(@props)
217
+ end
218
+
219
+ def call
220
+ content
221
+ end
222
+ end
223
+ end
@@ -13,13 +13,24 @@ class Fluxbit::TabComponent < Fluxbit::Component
13
13
  end
14
14
  }
15
15
 
16
+ # Initializes the Tab component with the given properties.
17
+ #
18
+ # @param [Hash] props The properties to customize the Tab component.
19
+ # @option props [Symbol] :variant (:default) The variant style (:default, :underline, :pills, :full_width).
20
+ # @option props [Symbol] :color (:blue) The color theme for active tabs.
21
+ # @option props [Boolean] :vertical (false) Whether tabs should be displayed vertically.
22
+ # @option props [Symbol] :tab_panel (:default) The style for tab panels (:default, :none).
23
+ # @option props [Symbol] :align (:left) Horizontal alignment of tabs (:left, :center, :right). Only applies to horizontal tabs.
24
+ # @option props [Hash] :ul_html ({}) HTML attributes to apply to the ul element.
25
+ # @option props [Hash] **props Remaining options declared as HTML attributes, applied to the tab container.
16
26
  def initialize(**props)
17
27
  @variant = (props.delete(:variant) || @@variant).to_sym
18
28
  @color = props.delete(:color) || @@color
19
29
  @vertical = props.delete(:vertical) || @@vertical
20
30
  @tab_panel = (props.delete(:tab_panel) || @@tab_panel).to_sym
31
+ @align = (props.delete(:align) || @@align).to_sym
21
32
  @tabs_group = []
22
- @ul_props = props.delete(:ul_props) || {}
33
+ @ul_html = props.delete(:ul_html) || {}
23
34
  @props = props
24
35
  @vertical = false if @variant == :full_width
25
36
  super
@@ -31,7 +42,7 @@ class Fluxbit::TabComponent < Fluxbit::Component
31
42
  add class: styles[:div][@vertical ? :vertical : :horizontal], to: @props, first_element: true
32
43
 
33
44
  if @has_panels
34
- content_tag :div, **@props do
45
+ tag.div(**@props) do
35
46
  concat(render_tab_list)
36
47
  concat(render_tab_panels)
37
48
  end
@@ -43,21 +54,22 @@ class Fluxbit::TabComponent < Fluxbit::Component
43
54
  private
44
55
 
45
56
  def render_tab_list
46
- add class: styles[:tab_list][:ul][@vertical ? :vertical : :horizontal], to: @ul_props, first_element: true
47
- add class: styles[:tab_list][:variant][variant], to: @ul_props
48
- @ul_props[:role] = "tablist"
57
+ add class: styles[:tab_list][:ul][@vertical ? :vertical : :horizontal], to: @ul_html, first_element: true
58
+ add class: styles[:tab_list][:variant][variant], to: @ul_html
59
+ add class: styles[:tab_list][:align][@align], to: @ul_html unless @vertical
60
+ @ul_html[:role] = "tablist"
49
61
 
50
62
  if @has_panels
51
- @ul_props[:data] = {
63
+ @ul_html[:data] = {
52
64
  "tabs-toggle": "##{fx_id}-content",
53
65
  "tabs-active-classes": styles[:tab_list][:tab_item][:variant][variant][:active][@color],
54
66
  "tabs-inactive-classes": styles[:tab_list][:tab_item][:variant][variant][:inactive]
55
67
  }
56
68
  end
57
69
 
58
- @ul_props[:id] = fx_id
70
+ @ul_html[:id] = fx_id
59
71
 
60
- content_tag :ul, **@ul_props do
72
+ tag.ul(**@ul_html) do
61
73
  safe_join(@tabs_group.map.with_index { |tab, index| render_tab(tab, index) })
62
74
  end
63
75
  end
@@ -102,41 +114,48 @@ class Fluxbit::TabComponent < Fluxbit::Component
102
114
  tab.props.delete :"aria-controls"
103
115
  end
104
116
 
105
- li_props = tab.props.delete(:li_props) || {}
106
- li_props[:role] = "presentation"
107
- li_props[:id] = "#{fx_id}-#{index}-li"
108
- add class: styles[:tab_list][:li], to: li_props, first_element: true
117
+ li_html = tab.props.delete(:li_html) || {}
118
+ li_html[:role] = "presentation"
119
+ li_html[:id] = "#{fx_id}-#{index}-li"
120
+ add class: styles[:tab_list][:li], to: li_html, first_element: true
109
121
 
110
- content_tag :li, **li_props do
111
- content_tag :a, **tab.props do
122
+ tag.li(**li_html) do
123
+ tag.a(**tab.props) do
112
124
  concat(render_icon(tab_icon)) if tab_icon
113
- concat(content_tag(:span, tab_title))
125
+ concat(tag.span(tab_title))
114
126
  end
115
127
  end
116
128
  end
117
129
 
118
130
  def render_icon(icon)
119
- if icon.include?('class="')
120
- icon.gsub("class=\"", "class=\"#{styles[:tab_list][:tab_item][:icon]} ")
131
+ return "" if icon.blank?
132
+
133
+ modified = if icon.include?('class="')
134
+ icon.gsub('class="', "class=\"#{styles[:tab_list][:tab_item][:icon]} ")
121
135
  else
122
136
  icon.gsub("<svg", "<svg class=\"#{styles[:tab_list][:tab_item][:icon]}\"")
123
- end.html_safe
137
+ end
138
+
139
+ allowed_tags = %w[svg path circle rect line polyline polygon g defs title use]
140
+ allowed_attrs = %w[class fill stroke stroke-width stroke-linecap stroke-linejoin width height viewBox xmlns x y d points cx cy r x1 y1 x2 y2 href aria-hidden]
141
+
142
+ view_context.sanitize(modified, tags: allowed_tags, attributes: allowed_attrs)
124
143
  end
125
144
 
126
145
  def render_tab_panels
127
- content_tag :div, id: "##{fx_id}-content", class: styles[:tabpanel_container][@vertical ? :vertical : :horizontal] do
146
+ tag.div(id: "##{fx_id}-content", class: styles[:tabpanel_container][@vertical ? :vertical : :horizontal]) do
128
147
  safe_join(@tabs_group.map.with_index { |tab, index| render_tabpanel(tab, index) })
129
148
  end
130
149
  end
131
150
 
132
151
  def render_tabpanel(tab, index)
133
- content_props = tab.props[:content_props] || {}
134
- add class: styles[:tabpanel][@vertical ? :vertical : :horizontal][@tab_panel][tab.props[:active] ? :active : :inactive], to: content_props, first_element: true
152
+ content_html = tab.props[:content_html] || {}
153
+ add class: styles[:tabpanel][@vertical ? :vertical : :horizontal][@tab_panel][tab.props[:active] ? :active : :inactive], to: content_html, first_element: true
135
154
 
136
- content_props[:id] = "#{fx_id}-tabpanel-#{index}"
137
- content_props[:role] = "tabpanel"
138
- content_props[:"aria-labelledby"] = "#{fx_id}-#{index}"
155
+ content_html[:id] = "#{fx_id}-tabpanel-#{index}"
156
+ content_html[:role] = "tabpanel"
157
+ content_html[:"aria-labelledby"] = "#{fx_id}-#{index}"
139
158
 
140
- content_tag :div, tab.content, **content_props
159
+ tag.div(tab.content, **content_html)
141
160
  end
142
161
  end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The `Fluxbit::TableComponent` is a customizable alert component that extends `Fluxbit::Component`.
5
+ # It provides various options to display alert messages with different styles, icons, and behaviors
6
+ # such as close functionality and animations.
7
+ #
8
+ # Example usage:
9
+ # = render Fluxbit::TableComponent.new(
10
+ # striped: true,
11
+ # bordered: true,
12
+ # hover: true,
13
+ # shadow: true,
14
+ # wrapper_html: { class: "my-custom-wrapper" },
15
+ # thead_html: { class: "my-custom-thead" },
16
+ # tbody_html: { class: "my-custom-tbody" },
17
+ # tr_html: { class: "my-custom-tr" }
18
+ # ) do |table|
19
+ # table.with_header do |header|
20
+ # header.with_column "Column 1"
21
+ # header.with_column "Column 2"
22
+ # end
23
+ #
24
+ # table.with_row do |row|
25
+ # row.with_cell "Data 1"
26
+ # row.with_cell "Data 2"
27
+ # end
28
+ # end
29
+ #
30
+ class Fluxbit::TableComponent < Fluxbit::Component
31
+ include Fluxbit::Config::TableComponent
32
+
33
+ renders_one :footer, lambda { |*args, **props, &block|
34
+ add class: styles[:footer][:base], to: props
35
+ props[:as] ||= :tr
36
+ remove_class_from_props props
37
+
38
+ props[:cells_html] ||= {}
39
+ props[:cells_html][:as] ||= :td
40
+ props[:cells_html][:scope] ||= :row
41
+ add(class: styles[:footer][:cell], to: props[:cells_html])
42
+ remove_class_from_props props[:cells_html]
43
+
44
+ Fluxbit::TableGroupComponent.new(*args, **props, &block)
45
+ }
46
+
47
+ renders_many :headers, lambda { |*args, **props, &block|
48
+ add class: styles[:head][:base], to: props
49
+ props[:as] ||= :tr
50
+ remove_class_from_props props
51
+
52
+ props[:cells_html] ||= {}
53
+ props[:cells_html][:as] ||= :th
54
+ props[:cells_html][:scope] ||= :col
55
+ add(class: styles[:head][:cell], to: props[:cells_html])
56
+ remove_class_from_props props[:cells_html]
57
+
58
+ Fluxbit::TableGroupComponent.new(*args, **props, &block)
59
+ }
60
+
61
+ renders_many :rows, lambda { |*args, **props, &block|
62
+ color = props.delete(:color) || :default
63
+ add(class: styles[:row][:base], to: props)
64
+ add(class: styles[:row][:striped][color], to: props) if @striped
65
+ add(class: styles[:row][:hovered][color], to: props) if @hover
66
+ add(class: styles[:row][:bordered], to: props) if @bordered
67
+ add(class: styles[:row][:colors][color], to: props, first_element: true) if styles[:row][:colors].key?(color) && !@striped
68
+ props[:as] ||= :tr
69
+ remove_class_from_props props
70
+
71
+ props[:cells_html] ||= {}
72
+ props[:cells_html][:as] ||= :td
73
+ props[:cells_html][:scope] ||= :row
74
+ add(class: styles[:row][:cell][:base], to: props[:cells_html])
75
+ remove_class_from_props props[:cells_html]
76
+
77
+ Fluxbit::TableGroupComponent.new(*args, **props, &block)
78
+ }
79
+
80
+ ##
81
+ # Initializes the table component with the given properties.
82
+ #
83
+ # @param [Hash] props The properties to customize the table.
84
+ # @option props [Boolean] :striped (false) Determines if the table rows should be striped.
85
+ # @option props [Boolean] :bordered (false) Determines if the table should have borders.
86
+ # @option props [Boolean] :hover (false) Determines if the table rows should highlight on hover.
87
+ # @option props [Boolean] :shadow (false) Determines if the table should have a shadow effect.
88
+ # @option props [Hash] :thead_html Additional HTML attributes for the table header.
89
+ # @option props [Hash] :tbody_html Additional HTML attributes for the table body.
90
+ # @option props [Hash] :tr_html Additional HTML attributes for the table rows.
91
+ # @option props [Hash] :cells_html Additional HTML attributes for the table cells.
92
+ #
93
+ # @example
94
+ # = render Fluxbit::TableComponent.new(
95
+ # striped: true,
96
+ # bordered: true,
97
+ # hover: true,
98
+ # shadow: true,
99
+ # thead_html: { class: "my-custom-thead" },
100
+ # tbody_html: { class: "my-custom-tbody" },
101
+ # tr_html: { class: "my-custom-tr" }
102
+ # ) do |table|
103
+ # table.with_header do |header|
104
+ # header.with_column "Column 1"
105
+ # header.with_column "Column 2"
106
+ # end
107
+ #
108
+ # table.with_row do |row|
109
+ # row.with_cell "Data 1"
110
+ # row.with_cell "Data 2"
111
+ # end
112
+ # end
113
+ #
114
+ # @return [Fluxbit::TableComponent]
115
+ #
116
+ def initialize(**props)
117
+ super
118
+ @props = props
119
+ @striped = options @props.delete(:striped), default: @@striped
120
+ @bordered = options @props.delete(:bordered), default: @@bordered
121
+ @hover = options @props.delete(:hover), default: @@hover
122
+ @shadow = options @props.delete(:shadow), default: @@shadow
123
+ @only_rows = options @props.delete(:only_rows), default: false
124
+
125
+ # Wrapper HTML
126
+ @wrapper_html = @props.delete(:wrapper_html) || {}
127
+ add(class: styles[:wrapper][:base], to: @wrapper_html)
128
+ add(class: styles[:wrapper][:shadow], to: @wrapper_html) if @shadow
129
+
130
+ # Head HTML
131
+ @thead_html = @props.delete(:thead_html) || {}
132
+ # add(class: styles[:head][:base], to: @thead_html)
133
+
134
+ # Body HTML
135
+ @tbody_html = @props.delete(:tbody_html) || {}
136
+ add(class: styles[:body][:base], to: @tbody_html)
137
+
138
+ # Row HTML
139
+ @tr_html = @props.delete(:tr_html) || {}
140
+ add(class: styles[:body][:base], to: @tbody_html)
141
+
142
+ # Footer HTML
143
+ @tfoot_html = @props.delete(:tfoot_html) || {}
144
+ add(class: styles[:footer][:base], to: @tfoot_html)
145
+
146
+ # Table HTML
147
+ add(class: styles[:root][:base], to: @props)
148
+ end
149
+
150
+ def call
151
+ return safe_join(rows) if @only_rows && rows?
152
+
153
+ # Wrapper
154
+ capture do
155
+ # Table
156
+ concat(tag.div(**@wrapper_html) do
157
+ concat(tag.table(**@props) do
158
+ # header
159
+ concat(
160
+ tag.thead(**@thead_html) do
161
+ concat(safe_join headers)
162
+ end
163
+ ) if headers?
164
+
165
+ # body
166
+ concat(
167
+ tag.tbody(**@tbody_html) do
168
+ if content.present?
169
+ content
170
+ else
171
+ concat(safe_join(rows)) if rows?
172
+ end
173
+ end
174
+ )
175
+
176
+ # Footer
177
+ concat(
178
+ tag.tfoot(**@tfoot_html) do
179
+ concat(footer)
180
+ end
181
+ ) if footer?
182
+ end)
183
+ end)
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fluxbit::TableGroupComponent < Fluxbit::Component
4
+ include Fluxbit::Config::TableComponent
5
+
6
+ renders_many :cells, lambda { |*args, **props, &block|
7
+ cells_html_now = @cells_html.dup
8
+ add(class: props[:class], to: cells_html_now)
9
+ cells_html_now = props.merge cells_html_now
10
+ add(class: styles[:row][:cell][:selected], to: cells_html_now) if props.delete(:selected) || false
11
+ remove_class_from_props(cells_html_now)
12
+
13
+ content_tag(@as_cells, block.call, **cells_html_now)
14
+ }
15
+
16
+ def initialize(**props)
17
+ super
18
+ @props = props
19
+ @as = @props.delete(:as) || :tr
20
+ @cells_html = @props.delete(:cells_html) || { as: :td }
21
+
22
+ @as_cells = @cells_html.delete(:as) || :td
23
+ end
24
+
25
+ def call
26
+ tag.tr(safe_join(cells), **@props)
27
+ end
28
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The `Fluxbit::ThemeButtonComponent` is a round button component that toggles between dark, light, and system themes.
5
+ # It extends `Fluxbit::ButtonComponent` and automatically updates the theme when clicked.
6
+ class Fluxbit::ThemeButtonComponent < Fluxbit::ButtonComponent
7
+ include Fluxbit::Config::ThemeButtonComponent
8
+
9
+ ##
10
+ # Initializes the theme button component with the given properties.
11
+ #
12
+ # @param [Hash] props The properties to customize the theme button.
13
+ # @option props [Symbol, String] :color The color style of the button (default: :transparent)
14
+ # @option props [Symbol, String] :size The size of the button (default: 2)
15
+ # @option props [Boolean] :pill (true) Makes the button round
16
+ # @option props [Hash] **props Remaining options declared as HTML attributes.
17
+ #
18
+ # @return [Fluxbit::ThemeButtonComponent]
19
+ def initialize(**props)
20
+ # Set default values specific to theme button
21
+ props[:pill] = true unless props.key?(:pill)
22
+ props[:color] ||= :transparent
23
+ props[:size] ||= 2
24
+ props[:remove_dropdown_arrow] = true
25
+
26
+ # Add Stimulus controller
27
+ props["data-controller"] = [props["data-controller"], "fx-theme-button"].compact.join(" ")
28
+ props["data-action"] = [props["data-action"], "click->fx-theme-button#toggle"].compact.join(" ")
29
+
30
+ super(**props)
31
+ end
32
+
33
+ def call
34
+ concat(render_theme_button)
35
+ concat(render_popover_or_tooltip.to_s)
36
+ end
37
+
38
+ private
39
+
40
+ def render_theme_button
41
+ button_content = safe_join([
42
+ content_tag(:span, light_icon, class: "fx-theme-icon fx-theme-light hidden", "data-fx-theme-button-target": "lightIcon"),
43
+ content_tag(:span, dark_icon, class: "fx-theme-icon fx-theme-dark hidden", "data-fx-theme-button-target": "darkIcon"),
44
+ content_tag(:span, system_icon, class: "fx-theme-icon fx-theme-system hidden", "data-fx-theme-button-target": "systemIcon")
45
+ ])
46
+
47
+ content_tag(@as, button_content, @props)
48
+ end
49
+
50
+ def light_icon
51
+ # Sun icon for light mode
52
+ anyicon("heroicons_outline:sun", class: "size-5")
53
+ end
54
+
55
+ def dark_icon
56
+ # Moon icon for dark mode
57
+ anyicon("heroicons_outline:moon", class: "size-5")
58
+ end
59
+
60
+ def system_icon
61
+ # Computer/display icon for system mode
62
+ anyicon("heroicons_outline:computer-desktop", class: "size-5")
63
+ end
64
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The `Fluxbit::TimelineComponent` is a component for rendering customizable timelines.
4
+ class Fluxbit::TimelineComponent < Fluxbit::Component
5
+ include Fluxbit::Config::TimelineComponent
6
+
7
+ # Slot for timeline items
8
+ renders_many :items, lambda { |**props|
9
+ Fluxbit::TimelineItemComponent.new(
10
+ variant: @variant,
11
+ **props
12
+ )
13
+ }
14
+
15
+ # Initializes the Timeline component with various customization options.
16
+ #
17
+ # @param [Hash] **props The properties to customize the timeline.
18
+ # @option props [Symbol] :variant (:default) The timeline variant (:default, :vertical, :stepper, :activity).
19
+ # @option props [Symbol] :position (:left) The position of timeline indicators (:left, :center, :right).
20
+ # @option props [String] :remove_class ('') CSS classes to remove from the default class list.
21
+ # @option props [Hash] **props Remaining options declared as HTML attributes.
22
+ #
23
+ # @return [Fluxbit::TimelineComponent]
24
+ def initialize(**props)
25
+ super
26
+ @props = props
27
+
28
+ @variant = options @props.delete(:variant), collection: styles[:variants].keys, default: @@variant
29
+ @position = options @props.delete(:position), collection: styles[:positions].keys, default: @@position
30
+
31
+ add class: [
32
+ styles[:base],
33
+ styles[:variants][@variant],
34
+ styles[:positions][@position]
35
+ ], to: @props
36
+
37
+ remove_class_from_props(@props)
38
+ end
39
+
40
+ def call
41
+ content_tag(@variant == :stepper ? "ol" : "ul", **@props) do
42
+ items_with_position.join.html_safe
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def items_with_position
49
+ items.map.with_index do |item, index|
50
+ item.instance_variable_set(:@is_last, index == items.length - 1)
51
+ item
52
+ end
53
+ end
54
+
55
+
56
+ def timeline_classes
57
+ [
58
+ styles[:base],
59
+ styles[:variants][@variant],
60
+ styles[:positions][@position]
61
+ ].compact.join(" ")
62
+ end
63
+ end