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
@@ -30,7 +30,13 @@ class Fluxbit::CardComponent < Fluxbit::Component
30
30
 
31
31
  renders_one :header
32
32
  renders_one :footer
33
- renders_one :section
33
+ renders_many :sections, lambda { |*args, **props, &block|
34
+ no_class = props.delete(:no_class) || false
35
+ add(class: styles[:body], to: props) unless no_class
36
+ # remove_class_from_props props
37
+
38
+ tag.div(block.call, **props)
39
+ }
34
40
 
35
41
  # Initializes the card component with various customization options.
36
42
  #
@@ -50,7 +56,7 @@ class Fluxbit::CardComponent < Fluxbit::Component
50
56
  # @param popover_trigger [String] Trigger event for the popover.
51
57
  # @param props [Hash] Additional HTML attributes for the container.
52
58
  def initialize(color: :default, shadow: true, border: true, rounded: true, hoverable: false,
53
- image: nil, image_position: :top, image_props: {},
59
+ image: nil, image_position: :top, image_html: {},
54
60
  tooltip_text: nil, tooltip_placement: "top", tooltip_trigger: "hover",
55
61
  popover_text: nil, popover_placement: "top", popover_trigger: "click",
56
62
  **props)
@@ -61,7 +67,7 @@ class Fluxbit::CardComponent < Fluxbit::Component
61
67
  @hoverable = hoverable
62
68
  @image = image
63
69
  @image_position = image_position.to_sym
64
- @image_props = image_props
70
+ @image_html = image_html
65
71
  @tooltip_text = tooltip_text
66
72
  @tooltip_placement = tooltip_placement
67
73
  @tooltip_trigger = tooltip_trigger
@@ -69,7 +75,7 @@ class Fluxbit::CardComponent < Fluxbit::Component
69
75
  @popover_placement = popover_placement
70
76
  @popover_trigger = popover_trigger
71
77
  @props = props
72
- @image_props[:src] = @image
78
+ @image_html[:src] = @image
73
79
  end
74
80
 
75
81
  def before_render
@@ -89,45 +95,42 @@ class Fluxbit::CardComponent < Fluxbit::Component
89
95
  def call
90
96
  container_tag = @props[:href] ? :a : :div
91
97
 
92
- header_html = header ? content_tag(:div, header, class: self.class.styles[:header]) : nil
93
- footer_html = footer ? content_tag(:div, footer, class: self.class.styles[:footer]) : nil
94
- body_content = section ? section : nil
98
+ header_html = header? ? tag.div(header, class: self.class.styles[:header]) : nil
99
+ footer_html = footer? ? tag.div(footer, class: self.class.styles[:footer]) : nil
100
+ body_content = content || safe_join(sections)
95
101
 
96
102
  if @image && @image_position == :top
97
103
  # Top image layout: image at the top, then header, body, and footer.
98
- add(class: styles[:image_top], to: @image_props)
99
- image_html = content_tag(:img, nil, **@image_props)
100
- body_html = body_content ? content_tag(:div, body_content, class: self.class.styles[:body]) : nil
104
+ add(class: styles[:image_top], to: @image_html)
105
+ image_html = tag.img(**@image_html)
101
106
 
102
107
  content_tag(container_tag, **@props) do
103
108
  concat(image_html)
104
109
  concat(header_html) if header_html
105
- concat(body_html) if body_html
110
+ concat(body_content) if body_content
111
+ concat(content) if content?
106
112
  concat(footer_html) if footer_html
107
113
  end
108
114
  elsif @image && @image_position == :left
109
115
  # Left image layout: image on the left and content on the right in a flex container.
110
- add(class: styles[:image_left], to: @image_props)
111
- image_html = content_tag(:div, class: "x") do
112
- content_tag(:img, nil, **@image_props)
113
- end
114
- content_inner = "".html_safe
115
- content_inner << header_html.to_s if header_html
116
- if body_content.present?
117
- content_inner << content_tag(:div, body_content, class: self.class.styles[:body] + " " + self.class.styles[:content_left])
116
+ add(class: styles[:image_left], to: @image_html)
117
+ image_html = tag.div(class: "x") do
118
+ tag.img(**@image_html)
118
119
  end
119
- content_inner << footer_html.to_s if footer_html
120
+ content_parts = []
121
+ content_parts << header_html if header_html
122
+ content_parts << body_content if body_content.present?
123
+ content_parts << footer_html if footer_html
120
124
 
121
125
  content_tag(container_tag, **@props) do
122
126
  concat(image_html)
123
- concat(content_tag(:div, content_inner, class: "flex-1"))
127
+ concat(tag.div(safe_join(content_parts), class: "flex-1"))
124
128
  end
125
129
  else
126
130
  # Fallback: render without image or with an unrecognized image_position.
127
- body_html = body_content ? content_tag(:div, body_content, class: self.class.styles[:body]) : nil
128
131
  content_tag(container_tag, **@props) do
129
132
  concat(header_html) if header_html
130
- concat(body_html) if body_html
133
+ concat(body_content) if body_content
131
134
  concat(footer_html) if footer_html
132
135
  end
133
136
  end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The `Fluxbit::CarouselComponent` is a customizable carousel component that extends `Fluxbit::Component`.
5
+ # It allows you to create image carousels or content sliders with various features like automatic sliding,
6
+ # indicators, navigation controls, and customizable animations.
7
+ #
8
+ # @example Basic usage
9
+ # = fx_carousel do |c|
10
+ # c.with_slide { image_tag("slide1.jpg") }
11
+ # c.with_slide { image_tag("slide2.jpg") }
12
+ # c.with_slide { image_tag("slide3.jpg") }
13
+ # end
14
+ #
15
+ # @see docs/02_Components/Carousel.md For detailed documentation.
16
+ class Fluxbit::CarouselComponent < Fluxbit::Component
17
+ include Fluxbit::Config::CarouselComponent
18
+
19
+ renders_many :slides, lambda { |**props, &block|
20
+ begin
21
+ @slides_group << ComponentObj.new(props, view_context.capture(&block))
22
+ rescue
23
+ @slides_group << ComponentObj.new(props, nil)
24
+ end
25
+ }
26
+
27
+ ##
28
+ # Initializes the carousel component with the given properties.
29
+ #
30
+ # @param [Hash] **props The properties to customize the component.
31
+ # @option props [Boolean] :slide (true) Enables automatic sliding between items.
32
+ # @option props [Integer] :slide_interval (3000) Interval in milliseconds between automatic slides.
33
+ # @option props [Boolean] :indicators (true) Shows slide indicator dots at the bottom.
34
+ # @option props [Boolean] :controls (true) Shows previous/next navigation controls.
35
+ # @option props [String] :left_control (nil) Custom content/text for left control button.
36
+ # @option props [String] :right_control (nil) Custom content/text for right control button.
37
+ # @option props [String] :remove_class ('') CSS classes to remove from the default class list.
38
+ # @option props [Hash] **props Remaining options declared as HTML attributes.
39
+ #
40
+ # @return [Fluxbit::CarouselComponent]
41
+ def initialize(**props)
42
+ super
43
+ @props = props
44
+ @slides_group = []
45
+
46
+ @slide = options(@props.delete(:slide), default: @@slide)
47
+ @slide_interval = options(@props.delete(:slide_interval), default: @@slide_interval)
48
+ @indicators = options(@props.delete(:indicators), default: @@indicators)
49
+ @controls = options(@props.delete(:controls), default: @@controls)
50
+ @left_control = @props.delete(:left_control)
51
+ @right_control = @props.delete(:right_control)
52
+
53
+ declare_classes
54
+
55
+ # Handle class removal
56
+ @props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
57
+
58
+ # Flowbite carousel data attributes
59
+ @props[:data] ||= {}
60
+ @props[:data][:carousel] = @slide ? "slide" : "static"
61
+ end
62
+
63
+ def call
64
+ slides # Ensure slides are rendered
65
+
66
+ tag.div(**@props) do
67
+ concat(render_slides_container)
68
+ concat(render_indicators) if @indicators
69
+ concat(render_controls) if @controls
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def declare_classes
76
+ add(class: styles[:base], to: @props, first_element: true)
77
+ end
78
+
79
+ def render_slides_container
80
+ tag.div(class: styles[:slides_container]) do
81
+ safe_join(@slides_group.map.with_index { |slide, index| render_slide(slide, index) })
82
+ end
83
+ end
84
+
85
+ def render_slide(slide, index)
86
+ slide_props = slide.props.dup
87
+ add(class: styles[:slide][:base], to: slide_props, first_element: true)
88
+ add(class: styles[:slide][:inactive], to: slide_props) unless index.zero?
89
+
90
+ slide_props[:data] ||= {}
91
+ slide_props[:data][:carousel_item] = index.zero? ? "active" : true
92
+
93
+ tag.div(**slide_props) do
94
+ slide.content
95
+ end
96
+ end
97
+
98
+ def render_indicators
99
+ tag.div(class: styles[:indicators][:container]) do
100
+ safe_join(@slides_group.count.times.map { |index| render_indicator(index) })
101
+ end
102
+ end
103
+
104
+ def render_indicator(index)
105
+ button_props = {
106
+ type: "button",
107
+ class: styles[:indicators][:button],
108
+ "aria-current": index.zero?.to_s,
109
+ "aria-label": "Slide #{index + 1}",
110
+ data: {
111
+ carousel_slide_to: index
112
+ }
113
+ }
114
+
115
+ tag.button(**button_props)
116
+ end
117
+
118
+ def render_controls
119
+ safe_join([
120
+ render_control_button(:previous),
121
+ render_control_button(:next)
122
+ ])
123
+ end
124
+
125
+ def render_control_button(direction)
126
+ is_previous = direction == :previous
127
+ custom_content = is_previous ? @left_control : @right_control
128
+
129
+ button_props = {
130
+ type: "button",
131
+ class: [styles[:controls][:button], is_previous ? styles[:controls][:previous] : styles[:controls][:next]].join(" "),
132
+ data: {}
133
+ }
134
+
135
+ # Add Flowbite carousel control data attribute
136
+ if is_previous
137
+ button_props[:data][:carousel_prev] = ""
138
+ else
139
+ button_props[:data][:carousel_next] = ""
140
+ end
141
+
142
+ tag.button(**button_props) do
143
+ if custom_content
144
+ custom_content
145
+ else
146
+ concat(tag.span(class: styles[:controls][:icon_wrapper]) do
147
+ is_previous ? chevron_left(class: styles[:controls][:icon]) : chevron_right(class: styles[:controls][:icon])
148
+ end)
149
+ concat(tag.span("#{direction.to_s.capitalize} slide", class: styles[:controls][:sr_only]))
150
+ end
151
+ end
152
+ end
153
+
154
+ end
@@ -3,6 +3,8 @@
3
3
  require "anyicon"
4
4
 
5
5
  class Fluxbit::Component < ViewComponent::Base
6
+ include Fluxbit::IconHelpers
7
+
6
8
  # Custom class to hold button properties and content
7
9
  ComponentObj = Data.define(:props, :content)
8
10
 
@@ -27,7 +29,14 @@ class Fluxbit::Component < ViewComponent::Base
27
29
  end
28
30
 
29
31
  def remove_class(elements, from)
30
- from.split.reject { |c| c.in?(elements.split) }.join(" ")
32
+ return "" if from.blank?
33
+ return from if elements.blank?
34
+
35
+ from.split.reject { |c| c.in?((elements || "").split) }.join(" ")
36
+ end
37
+
38
+ def remove_class_from_props(props)
39
+ props[:class] = remove_class(props.delete(:remove_class) || "", props[:class])
31
40
  end
32
41
 
33
42
  def options(value, collection: nil, default: nil)
@@ -45,6 +54,14 @@ class Fluxbit::Component < ViewComponent::Base
45
54
  ]
46
55
  end
47
56
 
57
+ def popover?
58
+ false # Override in components that support popovers via renders_one :popover
59
+ end
60
+
61
+ def tooltip?
62
+ false # Override in components that support tooltips via renders_one :tooltip
63
+ end
64
+
48
65
  def add_popover_or_tooltip
49
66
  if popover? || @popover_text.present?
50
67
  @props["data-popover-placement"] = @popover_placement
@@ -66,8 +83,12 @@ class Fluxbit::Component < ViewComponent::Base
66
83
  (0...10).map { ('a'..'z').to_a[rand(26)] }.join}_target"
67
84
  end
68
85
 
69
- def anyicon(icon:, **props)
70
- Anyicon::Icon.render(icon: icon, **props)
86
+ def anyicon(icon, **props)
87
+ Anyicon::Icon.render(icon, **props)
88
+ end
89
+
90
+ def icon(icon_name, **props)
91
+ anyicon(icon_name, **props)
71
92
  end
72
93
 
73
94
  def random_id
@@ -0,0 +1,30 @@
1
+ <%= tag.div(**@props) do %>
2
+ <% if @swipeable %>
3
+ <div class="p-4 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700" data-drawer-toggle="<%= @props[:id] %>">
4
+ <span class="absolute w-8 h-1 -translate-x-1/2 bg-gray-300 rounded-lg top-3 left-1/2 dark:bg-gray-600"></span>
5
+ <% end %>
6
+ <% if header? %>
7
+ <h5 id="<%= @props[:id] %>-label" class="inline-flex items-center mb-4 text-base font-semibold text-gray-500 dark:text-gray-400">
8
+ <%= header %>
9
+ </h5>
10
+ <% end %>
11
+ <% if @swipeable %></div><% end %>
12
+
13
+ <% if @show_close_button %>
14
+ <button type="button"
15
+ <% if @using_stimulus %>
16
+ data-action="click->fx-drawer#hide"
17
+ <% else %>
18
+ data-drawer-hide="<%= @props[:id] %>"
19
+ <% end %>
20
+ aria-controls="<%= @props[:id] %>"
21
+ class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm size-8 absolute top-2.5 end-2.5 flex items-center justify-center dark:hover:bg-gray-600 dark:hover:text-white"
22
+ >
23
+ <%= close_icon(class: "w-3 h-3") %>
24
+ <span class="sr-only"><%= t("fluxbit.drawer.dismiss") %></span>
25
+ </button>
26
+ <% end %>
27
+ <div>
28
+ <%= content %>
29
+ </div>
30
+ <% end %>
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The `Fluxbit::DrawerComponent` 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::DrawerComponent.new(
10
+ # placement: :left,
11
+ # sizing: :md,
12
+ # show_close_button: true,
13
+ # swipeable: false,
14
+ # shadow: true
15
+ # ) do |drawer|
16
+ # drawer.with_header do |header|
17
+ # "Header Content"
18
+ # end
19
+ # end
20
+ #
21
+ class Fluxbit::DrawerComponent < Fluxbit::Component
22
+ include Fluxbit::Config::DrawerComponent
23
+
24
+ renders_one :header, lambda { |*args, **props, &block|
25
+ # add(class: props[:class], to: @caption_html) if props[:class]
26
+ # @caption_html = props.merge(@caption_html)
27
+ # remove_class_from_props @caption_html
28
+
29
+ block.call
30
+ }
31
+
32
+ ##
33
+ # Initializes the table component with the given properties.
34
+ #
35
+ # @param [Hash] props The properties to customize the table.
36
+ # @option props [Boolean] :striped (false) Determines if the table rows should be striped.
37
+ # @option props [Boolean] :bordered (false) Determines if the table should have borders.
38
+ # @option props [Boolean] :hover (false) Determines if the table rows should highlight on hover.
39
+ # @option props [Boolean] :shadow (false) Determines if the table should have a shadow effect.
40
+ # @option props [Hash] :wrapper_html Additional HTML attributes for the wrapper div.
41
+ # @option props [Hash] :thead_html Additional HTML attributes for the table header.
42
+ # @option props [Hash] :tbody_html Additional HTML attributes for the table body.
43
+ # @option props [Hash] :tr_html Additional HTML attributes for the table rows.
44
+ # @option props [Hash] :cells_html Additional HTML attributes for the table cells.
45
+ #
46
+ # @example
47
+ # = render Fluxbit::DrawerComponent.new(
48
+ # placement: :left,
49
+ # sizing: :md,
50
+ # show_close_button: true,
51
+ # swipeable: false,
52
+ # shadow: true
53
+ # ) do |drawer|
54
+ # drawer.with_header do |header|
55
+ # "Header Content"
56
+ # end
57
+ # end
58
+ #
59
+ # @return [Fluxbit::DrawerComponent]
60
+ #
61
+ def initialize(**props)
62
+ super
63
+ @props = props
64
+ @placement = options(@props.delete(:placement), collection: styles[:placements].keys, default: @@placement).to_sym
65
+ @sizing = options(@props.delete(:sizing), collection: styles[:sizes][:horizontal].keys, default: @@sizing).to_sym
66
+ @show_close_button = options @props.delete(:show_close_button), default: @@show_close_button
67
+ @swipeable = options @props.delete(:swipeable), default: @@swipeable
68
+ @shadow = options @props.delete(:shadow), default: @@shadow
69
+ @backdrop = options @props.delete(:backdrop), default: @@backdrop
70
+ @auto_show = options @props.delete(:auto_show), default: @@auto_show
71
+ @body_scrolling = options @props.delete(:body_scrolling), default: @@body_scrolling
72
+ @edge_offset = @props.delete(:edge_offset)
73
+ @backdrop_classes = @props.delete(:backdrop_classes)
74
+
75
+ if @swipeable
76
+ @placement = :bottom
77
+ @show_close_button = false
78
+ end
79
+
80
+ @props[:id] ||= fx_id
81
+ @props[:tabindex] = "-1"
82
+ @props["aria-labelledby"] = "#{@props[:id]}-label"
83
+
84
+ @using_stimulus = @props["data-fx-drawer-target"].present? || @props["data-controller"].to_s.include?("fx-drawer") ||
85
+ (
86
+ @props[:data].present? && (
87
+ @props[:data][:"fx-drawer-target"].present? || (@props[:data][:controller] || "").to_s.include?("fx-drawer")
88
+ )
89
+ )
90
+
91
+ if @using_stimulus
92
+ if @props["data-controller"].to_s.include?("fx-drawer") || @props[:data][:controller].to_s.include?("fx-drawer")
93
+ @props["data-fx-drawer-auto-show-value"] = @auto_show
94
+ @props["data-fx-drawer-placement-value"] = @placement.to_s
95
+ @props["data-fx-drawer-backdrop-value"] = @backdrop
96
+ @props["data-fx-drawer-body-scrolling-value"] = @body_scrolling
97
+ @props["data-fx-drawer-edge-value"] = @swipeable
98
+ @props["data-fx-drawer-edge-offset-value"] = @edge_offset if @edge_offset.present? && @swipeable
99
+ @props["data-fx-drawer-backdrop-classes-value"] = @backdrop_classes if @backdrop_classes.present?
100
+ else
101
+ @props["data-auto-show"] = @auto_show
102
+ @props["data-placement"] = @placement.to_s
103
+ @props["data-backdrop"] = @backdrop
104
+ @props["data-body-scrolling"] = @body_scrolling
105
+ @props["data-edge"] = @swipeable
106
+ @props["data-edge-offset"] = @edge_offset if @edge_offset.present? && @swipeable
107
+ @props["data-backdrop-classes"] = @backdrop_classes if @backdrop_classes.present?
108
+ end
109
+ end
110
+
111
+ add(
112
+ class: [ styles[:root],
113
+ styles[:placements][@placement],
114
+ styles[:color],
115
+ @swipeable ? styles[:swipeable][:default] : nil,
116
+ @shadow ? styles[:shadow] : nil,
117
+ size_class ],
118
+ to: @props
119
+ )
120
+ end
121
+
122
+ def size_class
123
+ styles[:sizes][@placement.in?([ :left, :right ]) ? :horizontal : :vertical][@sizing]
124
+ end
125
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The `Fluxbit::DropdownComponent` is a component for rendering customizable Dropdown containers.
4
+ class Fluxbit::DropdownComponent < Fluxbit::Component
5
+ include Fluxbit::Config::DropdownComponent
6
+
7
+ renders_many :items, Fluxbit::DropdownItemComponent
8
+
9
+ # Initializes the Dropdown component with various customization options.
10
+ #
11
+ # @param sizing [Integer] The alignment of items. Defaults to `0` (small).
12
+ # @param divider [Boolean] Whether the Dropdown has a division between the items. Defaults to `true`.
13
+ # @param props [Hash] Additional HTML attributes for the container.
14
+ def initialize(**props)
15
+ @props = props
16
+ @sizing = props.delete(:sizing) || @@sizing
17
+ @auto_divider = options props.delete(:auto_divider), default: @@auto_divider
18
+ @props[:id] ||= "dropdown-#{random_id}"
19
+
20
+ add to: @props, class: [
21
+ styles[:base],
22
+ styles[:sizes][@sizing],
23
+ @auto_divider ? styles[:auto_divider] : nil
24
+ ]
25
+
26
+ remove_class_from_props(@props)
27
+ end
28
+
29
+ def get_item
30
+ @props[:id]
31
+ end
32
+
33
+ def call
34
+ tag.div(**@props) do
35
+ tag.ul(class: styles[:ul]) do
36
+ concat(safe_join(items)) if items.any?
37
+ concat(Fluxbit::DropdownItemComponent.new(content).render_in(view_context)) if content.present?
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The `Fluxbit::DropdownItemComponent` is a component for rendering customizable Dropdown item containers.
4
+ class Fluxbit::DropdownItemComponent < Fluxbit::Component
5
+ include Fluxbit::Config::DropdownComponent
6
+
7
+ renders_many :items, Fluxbit::DropdownItemComponent
8
+
9
+ def initialize(param_content = "", **props)
10
+ @param_content = param_content
11
+ @item_props = props
12
+ @type = @item_props.delete(:as) || :div
13
+ @height = @item_props.delete(:height) || @@height
14
+ @icon = @item_props.delete(:icon)
15
+ add to: @item_props, class: styles[:items][:heights][@height]
16
+ remove_class_from_props(@item_props)
17
+
18
+ @content_html = @item_props.delete(:content_html) || {}
19
+
20
+ @icon_html = @item_props.delete(:icon_html) || {}
21
+ add to: @icon_html, class: styles[:icon] if @icon.present?
22
+
23
+ @divider = @item_props.delete(:divider) || false
24
+ end
25
+
26
+ def call
27
+ return tag.li(class: styles[:divider]) if @divider
28
+
29
+ if items.any?
30
+ @content_html["data-dropdown-toggle"] = @item_props.delete(:dropdown_id) || "inner-dropdown-#{random_id}"
31
+ @content_html["data-dropdown-placement"] = @item_props.delete(:dropdown_placement) || "right-start"
32
+ height = @item_props.delete(:sizing) || 0
33
+ auto_divider = @item_props.delete(:auto_divider) || true
34
+ @type = :button
35
+ @content_html[:type] = "button"
36
+ end
37
+
38
+ add to: @content_html, class: styles[:items][:types][@type]
39
+ remove_class_from_props(@content_html)
40
+
41
+ tag.li(**@item_props) do
42
+ concat(content_tag(@type.to_sym, **@content_html) do
43
+ concat(tag.div(class: "flex") do
44
+ concat(anyicon(@icon, **@icon_html)) if @icon.present?
45
+ concat(content || @param_content)
46
+ end)
47
+ concat(chevron_right(class: "ms-3 ml-auto")) if items.any?
48
+ end)
49
+ concat(
50
+ inner_dropdown(items, id: @content_html["data-dropdown-toggle"], sizing: height, auto_divider: auto_divider)
51
+ ) if items.any?
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def inner_dropdown(items, id: "a", sizing: 0, auto_divider: true)
58
+ props = { id: id, class: styles[:base] }
59
+ add to: props, class: [
60
+ styles[:sizes][sizing],
61
+ auto_divider ? styles[:auto_divider] : nil
62
+ ]
63
+
64
+ tag.div(**props) do
65
+ tag.ul(safe_join(items), class: styles[:ul])
66
+ end
67
+ end
68
+ end
@@ -34,7 +34,7 @@ class Fluxbit::FlexComponent < Fluxbit::Component
34
34
  end
35
35
 
36
36
  def call
37
- content_tag :div, content, @props
37
+ tag.div(content, **@props)
38
38
  end
39
39
 
40
40
  private
@@ -15,7 +15,7 @@ class Fluxbit::Form::Component < Fluxbit::Component
15
15
  if help_text.nil? && !object.nil? && !attribute.nil?
16
16
  help_text = I18n.t(
17
17
  attribute,
18
- scope: [ :activerecord, :help_text, object.class.name.underscore.to_sym ],
18
+ scope: [ object.class.name.pluralize.underscore.to_sym, :help_text ],
19
19
  default: nil
20
20
  )
21
21
  end
@@ -24,14 +24,18 @@ class Fluxbit::Form::Component < Fluxbit::Component
24
24
  end
25
25
 
26
26
  def define_helper_popover(helper_popover, object, attribute)
27
- return helper_popover if (helper_popover != false && !helper_popover.nil?) || object.nil?
27
+ return nil if helper_popover == false
28
+ return helper_popover if !helper_popover.nil? || object.nil?
28
29
 
29
- object_name = object.class.name.underscore.to_sym
30
- I18n.t(attribute, scope: [ :activerecord, :helper_popover, object_name ], default: nil)
30
+ I18n.t(attribute, scope: [ object.class.name.pluralize.underscore.to_sym, :helper_popover ], default: nil)
31
31
  end
32
32
 
33
33
  def label_value(label, object, attribute, id)
34
- return object.class.human_attribute_name(attribute) if label.nil? && !object.nil? && !attribute.nil?
34
+ if label.nil? && !object.nil? && !attribute.nil?
35
+ key = [ object.class.name.pluralize.underscore.to_sym, :fields, attribute ]
36
+ return I18n.exists?(key) ? I18n.t(attribute, scope: key[0..-2]) : object.class.human_attribute_name(attribute)
37
+ end
38
+ return attribute.to_s.humanize if label.nil? && object.nil?
35
39
  return id.to_s.humanize if label.nil? && !id.nil?
36
40
  return label unless label.nil?
37
41
 
@@ -46,7 +50,8 @@ class Fluxbit::Form::Component < Fluxbit::Component
46
50
  color: @color,
47
51
  helper_popover: @helper_popover,
48
52
  helper_popover_placement: @helper_popover_placement,
49
- class: @label_class
53
+ class: @label_class,
54
+ required: @required
50
55
  ).with_content(@label).render_in(view_context)
51
56
  end
52
57
 
@@ -59,8 +64,10 @@ class Fluxbit::Form::Component < Fluxbit::Component
59
64
  def help_text
60
65
  return "" if @help_text.blank? || @help_text.compact.blank?
61
66
 
62
- @help_text.compact.map do |text|
67
+ nodes = @help_text.compact.map do |text|
63
68
  Fluxbit::Form::HelpTextComponent.new(color: @color).with_content(text).render_in(view_context)
64
- end.join.html_safe
69
+ end
70
+
71
+ view_context.safe_join(nodes)
65
72
  end
66
73
  end
@@ -19,7 +19,7 @@ class Fluxbit::Form::DropzoneComponent < Fluxbit::Form::FieldComponent
19
19
  # @param title [Boolean, String] Title text above the dropzone (true for default, false to hide, or custom string)
20
20
  # @param subtitle [Boolean, String] Subtitle text below the title (true for default, false to hide, or custom string)
21
21
  # @param icon [String, Symbol] Icon to display above the title (defaults to config)
22
- # @param icon_props [Hash] Extra props for the icon element
22
+ # @param icon_html [Hash] Extra props for the icon element
23
23
  # @param height [Integer] Height preset (0: auto, 1: h-32, 2: h-64, 3: h-96; default is 0)
24
24
  # @param help_text [String] Helper or error text below the field
25
25
  # @param ... any other HTML attribute supported by file_field_tag
@@ -28,12 +28,12 @@ class Fluxbit::Form::DropzoneComponent < Fluxbit::Form::FieldComponent
28
28
  @title = options(@props.delete(:title), default: true)
29
29
  @subtitle = options(@props.delete(:subtitle), default: true)
30
30
  @icon = @props.delete(:icon) || @@icon
31
- @icon_props = @props.delete(:icon_props) || { class: styles[:icon] }
31
+ @icon_html = @props.delete(:icon_html) || { class: styles[:icon] }
32
32
  @height = @props.delete(:height) || @@height
33
33
  add to: @props, class: "hidden"
34
34
  end
35
35
 
36
36
  def create_icon
37
- anyicon(icon: @icon, class: styles[:icon])
37
+ anyicon(@icon, class: styles[:icon])
38
38
  end
39
39
  end