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,437 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The `Fluxbit::BottomNavigationComponent` is a customizable bottom navigation component that extends `Fluxbit::Component`.
5
+ # It allows you to create fixed bottom navigation bars with multiple menu items, icons, and text labels.
6
+ #
7
+ # @example Basic usage
8
+ # = fx_bottom_navigation do |nav|
9
+ # nav.with_item(href: "/", icon: "heroicons_solid:home") { "Home" }
10
+ # nav.with_item(href: "/search", icon: "heroicons_solid:magnifying-glass") { "Search" }
11
+ # end
12
+ #
13
+ # @example With button group
14
+ # = fx_bottom_navigation do |nav|
15
+ # nav.with_button_group(columns: 3) do |group|
16
+ # group.with_button(active: true) { "New" }
17
+ # group.with_button { "Popular" }
18
+ # group.with_button { "Following" }
19
+ # end
20
+ # nav.with_item(href: "/", icon: "heroicons_solid:home") { "Home" }
21
+ # nav.with_item(href: "/wallet", icon: "heroicons_solid:wallet") { "Wallet" }
22
+ # end
23
+ #
24
+ # @see docs/02_Components/BottomNavigation.md For detailed documentation.
25
+ class Fluxbit::BottomNavigationComponent < Fluxbit::Component
26
+ include Fluxbit::Config::BottomNavigationComponent
27
+
28
+ renders_many :items, lambda { |**props, &block|
29
+ Item.new(**props, parent_config: self, parent_variant: @variant, &block)
30
+ }
31
+
32
+ renders_one :cta, lambda { |**props, &block|
33
+ CTA.new(**props, parent_config: self, &block)
34
+ }
35
+
36
+ renders_one :pagination, lambda { |**props, &block|
37
+ Pagination.new(**props, parent_config: self, &block)
38
+ }
39
+
40
+ renders_one :button_group, lambda { |**props, &block|
41
+ ButtonGroup.new(**props, parent_config: self, &block)
42
+ }
43
+
44
+ ##
45
+ # Initializes the bottom navigation component with the given properties.
46
+ #
47
+ # @param [Hash] **props The properties to customize the component.
48
+ # @option props [Symbol] :variant (:default) The style variant (:default or :app_bar).
49
+ # @option props [Boolean] :border (true) Shows border at the top of the navigation.
50
+ # @option props [String] :remove_class ('') CSS classes to remove from the default class list.
51
+ # @option props [Hash] **props Remaining options declared as HTML attributes.
52
+ #
53
+ # @return [Fluxbit::BottomNavigationComponent]
54
+ def initialize(**props)
55
+ super
56
+ @props = props
57
+
58
+ @variant = options(@props.delete(:variant), collection: styles[:variants].keys, default: @@variant)
59
+ @border = options(@props.delete(:border), default: @@border)
60
+ @remove_class = @props.delete(:remove_class) || ""
61
+ end
62
+
63
+ def call
64
+ items # Ensure items are rendered
65
+ cta # Ensure CTA is rendered if present
66
+ pagination # Ensure pagination is rendered if present
67
+ button_group # Ensure button_group is rendered if present
68
+
69
+ # Declare classes after button_group is rendered so we know which variant to use
70
+ declare_classes
71
+
72
+ # Handle class removal after classes are declared
73
+ @props[:class] = remove_class(@remove_class, @props[:class])
74
+
75
+ tag.div(**@props) do
76
+ safe_join([
77
+ (button_group if button_group?),
78
+ tag.div(class: container_classes) do
79
+ if cta?
80
+ # Insert CTA in the middle for both variants
81
+ half = (items.size / 2.0).floor
82
+ safe_join(items[0...half] + [tag.div(cta, class: styles[:cta_wrapper])] + items[half..])
83
+ elsif pagination?
84
+ # Insert pagination in the middle
85
+ half = (items.size / 2.0).floor
86
+ safe_join(items[0...half] + [pagination] + items[half..])
87
+ else
88
+ safe_join(items)
89
+ end
90
+ end
91
+ ].compact)
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def declare_classes
98
+ base_key = button_group? ? :base_with_button_group : :base
99
+ add(class: styles[:variants][@variant][base_key], to: @props, first_element: true)
100
+ add(class: styles[:variants][@variant][:border], to: @props) if @border && @variant == :default
101
+ end
102
+
103
+ def container_classes
104
+ base_key = button_group? ? :base_with_button_group : :base
105
+ base = styles[:container][base_key]
106
+
107
+ # Auto-calculate columns based on items count and presence of CTA/pagination
108
+ columns = calculate_columns
109
+
110
+ [
111
+ base,
112
+ styles[:container][:columns][columns - 2]
113
+ ].compact.join(" ")
114
+ end
115
+
116
+ def calculate_columns
117
+ # Count items + additional columns for CTA/pagination
118
+ total = items.size
119
+ total += 1 if cta? # CTA occupies 1 grid cell
120
+ total += 2 if pagination? # Pagination spans 2 grid cells (col-span-2)
121
+
122
+ # Ensure columns is within valid range (2-6)
123
+ [[total, 2].max, 6].min
124
+ end
125
+
126
+ ##
127
+ # Item component for bottom navigation
128
+ class Item < Fluxbit::Component
129
+ include Fluxbit::Config::BottomNavigationComponent
130
+
131
+ ##
132
+ # Initializes the item component.
133
+ #
134
+ # @param [Hash] **props The properties to customize the item.
135
+ # @option props [String] :href The URL the item links to.
136
+ # @option props [String] :icon The icon to display.
137
+ # @option props [Boolean] :active (false) Whether the item is currently active.
138
+ # @option props [String] :tooltip_text Tooltip text to display on hover.
139
+ # @option props [Boolean] :sr_only (false) Show text for screen readers only.
140
+ # @option props [Fluxbit::BottomNavigationComponent] :parent_config Parent component configuration.
141
+ # @option props [Symbol] :parent_variant Parent component variant.
142
+ # @option props [Hash] **props Remaining options declared as HTML attributes.
143
+ #
144
+ # @return [Item]
145
+ def initialize(**props, &block)
146
+ super(**props, &block)
147
+ @props = props
148
+ @parent_config = @props.delete(:parent_config)
149
+ @parent_variant = @props.delete(:parent_variant) || :default
150
+
151
+ @href = @props.delete(:href) || "#"
152
+ @icon = @props.delete(:icon)
153
+ @active = options(@props.delete(:active), default: false)
154
+ @tooltip_text = @props.delete(:tooltip_text)
155
+ @sr_only = options(@props.delete(:sr_only), default: @parent_variant == :app_bar)
156
+
157
+ add(class: styles[:item][:base], to: @props, first_element: true)
158
+ add(class: styles[:item][:active], to: @props) if @active
159
+ add(class: styles[:item][:inactive], to: @props) unless @active
160
+
161
+ @props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
162
+ end
163
+
164
+ def call
165
+ setup_data_attributes
166
+
167
+ button_content = tag.button(type: "button", **@props) do
168
+ concat(render_icon) if @icon
169
+ concat(tag.span(content, class: text_classes))
170
+ end
171
+
172
+ if @tooltip_text
173
+ safe_join([button_content, render_tooltip])
174
+ else
175
+ button_content
176
+ end
177
+ end
178
+
179
+ private
180
+
181
+ def render_icon
182
+ return "" if @icon.blank?
183
+
184
+ tag.div(class: styles[:item][:icon_wrapper]) do
185
+ anyicon(@icon, class: styles[:item][:icon])
186
+ end
187
+ end
188
+
189
+ def text_classes
190
+ @sr_only ? styles[:item][:sr_only] : styles[:item][:text]
191
+ end
192
+
193
+ def setup_data_attributes
194
+ @props[:data] ||= {}
195
+ @props[:data][:href] = @href
196
+
197
+ if @tooltip_text
198
+ tooltip_id = "tooltip-#{content.to_s.parameterize}"
199
+ @props[:data][:"tooltip-target"] = tooltip_id
200
+ end
201
+ end
202
+
203
+ def render_tooltip
204
+ tooltip_id = "tooltip-#{content.to_s.parameterize}"
205
+
206
+ tag.div(id: tooltip_id, role: "tooltip", class: styles[:tooltip][:base]) do
207
+ concat(@tooltip_text)
208
+ concat(tag.div(class: styles[:tooltip][:arrow], data: { tooltip_arrow: "" }))
209
+ end
210
+ end
211
+ end
212
+
213
+ ##
214
+ # CTA component for app bar variant
215
+ class CTA < Fluxbit::Component
216
+ include Fluxbit::Config::BottomNavigationComponent
217
+
218
+ ##
219
+ # Initializes the CTA component.
220
+ #
221
+ # @param [Hash] **props The properties to customize the CTA.
222
+ # @option props [String] :href The URL the CTA links to.
223
+ # @option props [String] :icon The icon to display.
224
+ # @option props [String] :tooltip_text Tooltip text to display on hover.
225
+ # @option props [Hash] **props Remaining options declared as HTML attributes.
226
+ #
227
+ # @return [CTA]
228
+ def initialize(**props, &block)
229
+ super(**props, &block)
230
+ @props = props
231
+ @parent_config = @props.delete(:parent_config)
232
+
233
+ @href = @props.delete(:href) || "#"
234
+ @icon = @props.delete(:icon)
235
+ @tooltip_text = @props.delete(:tooltip_text)
236
+
237
+ add(class: styles[:cta][:button], to: @props, first_element: true)
238
+
239
+ @props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
240
+ end
241
+
242
+ def call
243
+ setup_data_attributes
244
+
245
+ button_content = tag.button(type: "button", **@props) do
246
+ safe_join([
247
+ (@icon ? anyicon(@icon, class: styles[:cta][:icon]) : nil),
248
+ tag.span(content, class: styles[:item][:sr_only])
249
+ ].compact)
250
+ end
251
+
252
+ if @tooltip_text
253
+ safe_join([button_content, render_tooltip])
254
+ else
255
+ button_content
256
+ end
257
+ end
258
+
259
+ private
260
+
261
+ def setup_data_attributes
262
+ @props[:data] ||= {}
263
+ @props[:data][:href] = @href
264
+
265
+ if @tooltip_text
266
+ tooltip_id = "tooltip-#{content.to_s.parameterize}"
267
+ @props[:data][:"tooltip-target"] = tooltip_id
268
+ end
269
+ end
270
+
271
+ def render_tooltip
272
+ tooltip_id = "tooltip-#{content.to_s.parameterize}"
273
+
274
+ tag.div(id: tooltip_id, role: "tooltip", class: styles[:tooltip][:base]) do
275
+ concat(@tooltip_text)
276
+ concat(tag.div(class: styles[:tooltip][:arrow], data: { tooltip_arrow: "" }))
277
+ end
278
+ end
279
+ end
280
+
281
+ ##
282
+ # Pagination component for bottom navigation
283
+ class Pagination < Fluxbit::Component
284
+ include Fluxbit::Config::BottomNavigationComponent
285
+
286
+ ##
287
+ # Initializes the pagination component.
288
+ #
289
+ # @param [Hash] **props The properties to customize the pagination.
290
+ # @option props [Integer] :current_page (1) The current page number.
291
+ # @option props [Integer] :total_pages (1) The total number of pages.
292
+ # @option props [String] :previous_href The URL for the previous page.
293
+ # @option props [String] :next_href The URL for the next page.
294
+ # @option props [String] :previous_label ("Previous") The label for the previous button.
295
+ # @option props [String] :next_label ("Next") The label for the next button.
296
+ # @option props [Hash] **props Remaining options declared as HTML attributes.
297
+ #
298
+ # @return [Pagination]
299
+ def initialize(**props, &block)
300
+ super(**props, &block)
301
+ @props = props
302
+ @parent_config = @props.delete(:parent_config)
303
+
304
+ @current_page = @props.delete(:current_page) || 1
305
+ @total_pages = @props.delete(:total_pages) || 1
306
+ @previous_href = @props.delete(:previous_href) || "#"
307
+ @next_href = @props.delete(:next_href) || "#"
308
+ @previous_label = @props.delete(:previous_label) || "Previous"
309
+ @next_label = @props.delete(:next_label) || "Next"
310
+ end
311
+
312
+ def call
313
+ tag.div(class: styles[:pagination][:container]) do
314
+ safe_join([
315
+ render_previous_button,
316
+ render_page_info,
317
+ render_next_button
318
+ ])
319
+ end
320
+ end
321
+
322
+ private
323
+
324
+ def render_previous_button
325
+ tag.button(
326
+ type: "button",
327
+ class: styles[:pagination][:button],
328
+ data: { href: @previous_href },
329
+ disabled: @current_page <= 1
330
+ ) do
331
+ concat(chevron_left(class: styles[:pagination][:icon]))
332
+ concat(tag.span(@previous_label, class: styles[:pagination][:sr_only]))
333
+ end
334
+ end
335
+
336
+ def render_next_button
337
+ tag.button(
338
+ type: "button",
339
+ class: styles[:pagination][:button],
340
+ data: { href: @next_href },
341
+ disabled: @current_page >= @total_pages
342
+ ) do
343
+ concat(tag.span(@next_label, class: styles[:pagination][:sr_only]))
344
+ concat(chevron_right(class: styles[:pagination][:icon]))
345
+ end
346
+ end
347
+
348
+ def render_page_info
349
+ tag.div(class: styles[:pagination][:info]) do
350
+ "#{@current_page} of #{@total_pages}"
351
+ end
352
+ end
353
+ end
354
+
355
+ ##
356
+ # ButtonGroup component for bottom navigation
357
+ class ButtonGroup < Fluxbit::Component
358
+ include Fluxbit::Config::BottomNavigationComponent
359
+
360
+ renders_many :buttons, lambda { |**props, &block|
361
+ Button.new(**props, parent_config: self, &block)
362
+ }
363
+
364
+ ##
365
+ # Initializes the button group component.
366
+ #
367
+ # @param [Hash] **props The properties to customize the button group.
368
+ # @option props [Integer] :columns (3) Number of columns for button grid (2-5).
369
+ # @option props [Hash] **props Remaining options declared as HTML attributes.
370
+ #
371
+ # @return [ButtonGroup]
372
+ def initialize(**props, &block)
373
+ super(**props, &block)
374
+ @props = props
375
+ @parent_config = @props.delete(:parent_config)
376
+ @columns = @props.delete(:columns) || 3
377
+ end
378
+
379
+ def call
380
+ buttons # Ensure buttons are rendered
381
+
382
+ tag.div(class: styles[:button_group][:container], role: "group") do
383
+ tag.div(class: button_group_grid_classes) do
384
+ safe_join(buttons)
385
+ end
386
+ end
387
+ end
388
+
389
+ private
390
+
391
+ def button_group_grid_classes
392
+ [
393
+ styles[:button_group][:grid],
394
+ styles[:button_group][:columns][@columns - 2]
395
+ ].compact.join(" ")
396
+ end
397
+
398
+ ##
399
+ # Button component for button group
400
+ class Button < Fluxbit::Component
401
+ include Fluxbit::Config::BottomNavigationComponent
402
+
403
+ ##
404
+ # Initializes the button component.
405
+ #
406
+ # @param [Hash] **props The properties to customize the button.
407
+ # @option props [String] :href The URL the button links to.
408
+ # @option props [Boolean] :active (false) Whether the button is currently active.
409
+ # @option props [Hash] **props Remaining options declared as HTML attributes.
410
+ #
411
+ # @return [Button]
412
+ def initialize(**props, &block)
413
+ super(**props, &block)
414
+ @props = props
415
+ @parent_config = @props.delete(:parent_config)
416
+
417
+ @href = @props.delete(:href) || "#"
418
+ @active = options(@props.delete(:active), default: false)
419
+
420
+ add(class: styles[:button_group][:button], to: @props, first_element: true)
421
+ add(class: styles[:button_group][:button_active], to: @props) if @active
422
+ add(class: styles[:button_group][:button_inactive], to: @props) unless @active
423
+
424
+ @props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
425
+ end
426
+
427
+ def call
428
+ @props[:data] ||= {}
429
+ @props[:data][:href] = @href
430
+
431
+ tag.button(type: "button", **@props) do
432
+ content
433
+ end
434
+ end
435
+ end
436
+ end
437
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The `Fluxbit::BreadcrumbComponent` renders a breadcrumb navigation following Flowbite styles.
4
+ # It uses slots for items, allowing each breadcrumb item to have an optional icon and a current_page flag.
5
+ class Fluxbit::BreadcrumbComponent < Fluxbit::Component
6
+ # Include configuration (default classes, etc.) for Breadcrumb, if defined
7
+ include Fluxbit::Config::BreadcrumbComponent
8
+
9
+ renders_many :items, lambda { |**attrs, &block|
10
+ item = Item.new(**attrs)
11
+ item.with_content(block.call) if block_given?
12
+ item
13
+ }
14
+
15
+ # Initialize with any HTML attributes (e.g., custom class or aria-label for the <nav>)
16
+ def initialize(**props)
17
+ super
18
+ @props = props
19
+ @props["aria-label"] ||= "Breadcrumb"
20
+ end
21
+
22
+ def call
23
+ tag.nav(**@props) do
24
+ tag.ol(safe_join(items), class: styles[:root][:list])
25
+ end
26
+ end
27
+
28
+ class Item < Fluxbit::Component
29
+ include Fluxbit::Config::BreadcrumbComponent
30
+ renders_one :dropdown, Fluxbit::DropdownComponent
31
+
32
+ def initialize(**props)
33
+ super
34
+ @props = props
35
+ @current_page = @props.delete(:current_page)
36
+ @href = @props.delete(:href)
37
+ @icon = @props.delete(:icon)
38
+ @remove_dropdown_arrow = options(@props.delete(:remove_dropdown_arrow), default: false)
39
+ end
40
+
41
+ def call
42
+ item_content = content || ""
43
+ add class: styles[:item][:href][(@current_page || @href.blank?) ? :off : :on], to: @props
44
+ if dropdown? && !@remove_dropdown_arrow
45
+ item_content += chevron_down(class: "ms-3")
46
+ add class: styles[:item][:click_cursor], to: @props
47
+ end
48
+
49
+ @props["data-dropdown-toggle"] = dropdown.get_item if dropdown?
50
+
51
+ tag.li(class: styles[:item][:base]) do
52
+ concat chevron_right(class: styles[:item][:chevron], stroke_width: 1)
53
+ if @current_page || @href.blank?
54
+ concat anyicon(@icon, class: styles[:item][:icon]) if @icon
55
+ concat tag.span(item_content, **@props)
56
+ else
57
+ concat(tag.a(href: @href, **@props) do
58
+ concat(anyicon(@icon, class: styles[:item][:icon])) if @icon
59
+ concat item_content
60
+ end)
61
+ end
62
+ concat(dropdown&.to_s || "")
63
+ end
64
+ end
65
+ end
66
+ end
@@ -14,6 +14,7 @@ class Fluxbit::ButtonComponent < Fluxbit::Component
14
14
  @tooltip_props = props
15
15
  @tooltip_text = block.call
16
16
  }
17
+ renders_one :dropdown, Fluxbit::DropdownComponent
17
18
 
18
19
  ##
19
20
  # Initializes the button component with the given properties.
@@ -25,6 +26,7 @@ class Fluxbit::ButtonComponent < Fluxbit::Component
25
26
  # @option props [Symbol, String] :color The color style of the button.
26
27
  # @option props [Symbol, String] :size The size of the button (e.g., `0` to `4`).
27
28
  # @option props [Boolean] :disabled (false) Sets the button to a disabled state.
29
+ # @option props [Boolean] :selected (false) Makes the button appear darker to indicate selected state.
28
30
  # @option props [String] :remove_class ('') Classes to remove from the default class list.
29
31
  # @option props [String] :popover_text (nil) Popover text (from Fluxbit::Component).
30
32
  # @option props [Symbol] :popover_placement (:right) Popover placement (e.g., `:right`, `:left`, `:top`, `:bottom`) (from Fluxbit::Component).
@@ -39,15 +41,20 @@ class Fluxbit::ButtonComponent < Fluxbit::Component
39
41
  super
40
42
  @props = props
41
43
  @form = @props.delete(:form)
42
- @as = @props.delete(:as) || @@as
43
- @pill = @props.delete(:pill) || @@pill
44
- @color = @props.delete(:color) || @@color
45
- @grouped = @props.delete(:grouped) || false
46
- @first_button = @props.delete(:first_button) || false
47
- @last_button = @props.delete(:last_button) || false
48
- @outline = @color.end_with?("_outline")
44
+ @content = @props.delete(:content)
45
+ @as = options @props.delete(:as), default: @@as
46
+ @pill = options @props.delete(:pill), default: @@pill
47
+ @color = options (@props.delete(:color) || "").to_sym, collection: styles[:colors].keys, default: @@color
48
+ @selected = options @props.delete(:selected), default: @@selected
49
+ @grouped = options @props.delete(:grouped), default: false
50
+ @first_button = options @props.delete(:first_button), default: false
51
+ @last_button = options @props.delete(:last_button), default: false
52
+ @outline = @color.to_s.end_with?("_outline")
53
+ @full_sized = options(@props.delete(:full_sized), default: true)
54
+ @remove_dropdown_arrow = options(@props.delete(:remove_dropdown_arrow), default: false)
49
55
  declare_size(@props.delete(:size) || @@size)
50
56
  declare_disabled
57
+ declare_selected
51
58
  declare_classes
52
59
  @props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
53
60
  end
@@ -72,6 +79,7 @@ class Fluxbit::ButtonComponent < Fluxbit::Component
72
79
  to: @props,
73
80
  first_element: true
74
81
  )
82
+ add(class: styles[:full_sized], to: @props) if @full_sized
75
83
  add(class: styles[:inner][:base], to: @props) if @grouped
76
84
  add(class: styles[:inner][:position][:start], to: @props) if @grouped && @first_button
77
85
  add(class: styles[:inner][:position][:end], to: @props) if @grouped && @last_button
@@ -84,14 +92,34 @@ class Fluxbit::ButtonComponent < Fluxbit::Component
84
92
  add(class: styles[:disabled], to: @props, first_element: true)
85
93
  end
86
94
 
95
+ def declare_selected
96
+ return unless @selected.present? && @selected == true
97
+
98
+ add(class: styles[:selected], to: @props, first_element: true)
99
+ end
100
+
87
101
  def before_render
88
102
  add_popover_or_tooltip
89
103
  end
90
104
 
91
105
  def call
92
- concat(
93
- (@form.nil? ? content_tag(@as, content, @props) : @form.submit(**@props)) +
94
- render_popover_or_tooltip.to_s
95
- )
106
+ @props["data-dropdown-toggle"] = dropdown.get_item if dropdown?
107
+
108
+ concat(render_button)
109
+ concat(render_popover_or_tooltip.to_s)
110
+ concat(dropdown&.to_s || "")
111
+ end
112
+
113
+ private
114
+
115
+ def render_button
116
+ button_content = @content || content || ""
117
+
118
+ if @form.nil?
119
+ button_content += dropdown? && !@remove_dropdown_arrow ? chevron_down(class: "ms-3") : ""
120
+ content_tag(@as, button_content, @props)
121
+ else
122
+ @form.submit(button_content, **@props)
123
+ end
96
124
  end
97
125
  end
@@ -27,7 +27,7 @@ class Fluxbit::ButtonGroupComponent < Fluxbit::Component
27
27
 
28
28
  def call
29
29
  buttons
30
- content_tag :div, **@props do
30
+ tag.div(**@props) do
31
31
  @buttons_group.each_with_index do |button, index|
32
32
  button_props = button.props || {}
33
33
  button_content = button.content