fluxbit_view_components 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -0
  3. data/app/assets/javascripts/fluxbit_view_components/assigner_controller.js +49 -0
  4. data/app/assets/javascripts/fluxbit_view_components/auto_submit_controller.js +39 -0
  5. data/app/assets/javascripts/fluxbit_view_components/drawer_controller.js +135 -0
  6. data/app/assets/javascripts/fluxbit_view_components/index.js +56 -0
  7. data/app/assets/javascripts/fluxbit_view_components/method_link_controller.js +143 -0
  8. data/app/assets/javascripts/fluxbit_view_components/modal_controller.js +118 -0
  9. data/app/assets/javascripts/fluxbit_view_components/password_controller.js +170 -0
  10. data/app/assets/javascripts/fluxbit_view_components/progress_controller.js +374 -0
  11. data/app/assets/javascripts/fluxbit_view_components/row_click_controller.js +32 -0
  12. data/app/assets/javascripts/fluxbit_view_components/select_all_controller.js +122 -0
  13. data/app/assets/javascripts/fluxbit_view_components/spinner_percent_controller.js +174 -0
  14. data/app/assets/javascripts/fluxbit_view_components/theme_button_controller.js +90 -0
  15. data/app/assets/javascripts/fluxbit_view_components.js +1175 -0
  16. data/app/components/fluxbit/accordion_component.rb +125 -0
  17. data/app/components/fluxbit/alert_component.rb +8 -8
  18. data/app/components/fluxbit/avatar_component.rb +11 -12
  19. data/app/components/fluxbit/avatar_group_component.rb +1 -1
  20. data/app/components/fluxbit/badge_component.rb +8 -7
  21. data/app/components/fluxbit/banner_component.rb +139 -0
  22. data/app/components/fluxbit/bottom_navigation_component.rb +437 -0
  23. data/app/components/fluxbit/breadcrumb_component.rb +66 -0
  24. data/app/components/fluxbit/button_component.rb +39 -11
  25. data/app/components/fluxbit/button_group_component.rb +1 -1
  26. data/app/components/fluxbit/card_component.rb +26 -23
  27. data/app/components/fluxbit/carousel_component.rb +154 -0
  28. data/app/components/fluxbit/component.rb +24 -3
  29. data/app/components/fluxbit/drawer_component.html.erb +30 -0
  30. data/app/components/fluxbit/drawer_component.rb +125 -0
  31. data/app/components/fluxbit/dropdown_component.rb +41 -0
  32. data/app/components/fluxbit/dropdown_item_component.rb +68 -0
  33. data/app/components/fluxbit/flex_component.rb +1 -1
  34. data/app/components/fluxbit/form/check_box_component.rb +56 -0
  35. data/app/components/fluxbit/form/component.rb +27 -26
  36. data/app/components/fluxbit/form/dropzone_component.html.erb +39 -0
  37. data/app/components/fluxbit/form/dropzone_component.rb +39 -0
  38. data/app/components/fluxbit/form/field_component.rb +28 -0
  39. data/app/components/fluxbit/form/form_builder_component.rb +1 -1
  40. data/app/components/fluxbit/form/{helper_text_component.rb → help_text_component.rb} +9 -4
  41. data/app/components/fluxbit/form/label_component.rb +40 -30
  42. data/app/components/fluxbit/form/password_component.rb +247 -0
  43. data/app/components/fluxbit/form/radio_group_button_component.rb +126 -0
  44. data/app/components/fluxbit/form/range_component.rb +52 -0
  45. data/app/components/fluxbit/form/select_component.rb +185 -0
  46. data/app/components/fluxbit/form/text_field_component.rb +185 -0
  47. data/app/components/fluxbit/form/toggle_component.html.erb +23 -0
  48. data/app/components/fluxbit/form/toggle_component.rb +81 -0
  49. data/app/components/fluxbit/form/upload_image_component.html.erb +50 -0
  50. data/app/components/fluxbit/form/upload_image_component.rb +61 -0
  51. data/app/components/fluxbit/gravatar_component.rb +7 -0
  52. data/app/components/fluxbit/icon_helpers.rb +167 -0
  53. data/app/components/fluxbit/link_component.rb +42 -0
  54. data/app/components/fluxbit/modal_component.rb +28 -31
  55. data/app/components/fluxbit/pagination_component.rb +206 -0
  56. data/app/components/fluxbit/popover_component.rb +14 -14
  57. data/app/components/fluxbit/progress_component.rb +196 -0
  58. data/app/components/fluxbit/skeleton_component.rb +237 -0
  59. data/app/components/fluxbit/speed_dial_action_component.html.erb +30 -0
  60. data/app/components/fluxbit/speed_dial_action_component.rb +59 -0
  61. data/app/components/fluxbit/speed_dial_component.html.erb +33 -0
  62. data/app/components/fluxbit/speed_dial_component.rb +73 -0
  63. data/app/components/fluxbit/spinner_component.rb +71 -0
  64. data/app/components/fluxbit/spinner_percent_component.rb +174 -0
  65. data/app/components/fluxbit/stepper_component.rb +223 -0
  66. data/app/components/fluxbit/tab_component.rb +44 -25
  67. data/app/components/fluxbit/table_component.rb +186 -0
  68. data/app/components/fluxbit/table_group_component.rb +28 -0
  69. data/app/components/fluxbit/theme_button_component.rb +64 -0
  70. data/app/components/fluxbit/timeline_component.rb +63 -0
  71. data/app/components/fluxbit/timeline_item_component.html.erb +64 -0
  72. data/app/components/fluxbit/timeline_item_component.rb +78 -0
  73. data/app/components/fluxbit/tooltip_component.rb +2 -2
  74. data/app/helpers/fluxbit/components_helper.rb +93 -51
  75. data/app/helpers/fluxbit/form_builder.rb +136 -0
  76. data/app/helpers/fluxbit/view_helper.rb +71 -0
  77. data/config/locales/en.yml +37 -4
  78. data/config/locales/pt-BR.yml +36 -0
  79. data/lib/fluxbit/config/accordion_component.rb +73 -0
  80. data/lib/fluxbit/config/avatar_component.rb +11 -11
  81. data/lib/fluxbit/config/badge_component.rb +14 -11
  82. data/lib/fluxbit/config/banner_component.rb +60 -0
  83. data/lib/fluxbit/config/bottom_navigation_component.rb +74 -0
  84. data/lib/fluxbit/config/breadcrumb_component.rb +24 -0
  85. data/lib/fluxbit/config/button_component.rb +6 -4
  86. data/lib/fluxbit/config/card_component.rb +23 -12
  87. data/lib/fluxbit/config/carousel_component.rb +33 -0
  88. data/lib/fluxbit/config/drawer_component.rb +48 -0
  89. data/lib/fluxbit/config/dropdown_component.rb +29 -0
  90. data/lib/fluxbit/config/form/check_box_component.rb +19 -0
  91. data/lib/fluxbit/config/form/dropzone_component.rb +20 -0
  92. data/lib/fluxbit/config/form/{helper_text_component.rb → help_text_component.rb} +2 -2
  93. data/lib/fluxbit/config/form/label_component.rb +31 -0
  94. data/lib/fluxbit/config/form/password_component.rb +19 -0
  95. data/lib/fluxbit/config/form/radio_group_button_component.rb +24 -0
  96. data/lib/fluxbit/config/form/range_component.rb +15 -0
  97. data/lib/fluxbit/config/form/text_field_component.rb +76 -0
  98. data/lib/fluxbit/config/form/toggle_component.rb +79 -0
  99. data/lib/fluxbit/config/link_component.rb +24 -0
  100. data/lib/fluxbit/config/modal_component.rb +1 -1
  101. data/lib/fluxbit/config/pagination_component.rb +31 -0
  102. data/lib/fluxbit/config/popover_component.rb +1 -1
  103. data/lib/fluxbit/config/progress_component.rb +63 -0
  104. data/lib/fluxbit/config/skeleton_component.rb +82 -0
  105. data/lib/fluxbit/config/speed_dial_component.rb +50 -0
  106. data/lib/fluxbit/config/spinner_component.rb +30 -0
  107. data/lib/fluxbit/config/spinner_percent_component.rb +61 -0
  108. data/lib/fluxbit/config/stepper_component.rb +299 -0
  109. data/lib/fluxbit/config/tab_component.rb +6 -0
  110. data/lib/fluxbit/config/table_component.rb +75 -0
  111. data/lib/fluxbit/config/theme_button_component.rb +19 -0
  112. data/lib/fluxbit/config/timeline_component.rb +77 -0
  113. data/lib/fluxbit/view_components/engine.rb +11 -3
  114. data/lib/fluxbit/view_components/version.rb +1 -1
  115. data/lib/fluxbit/view_components.rb +27 -1
  116. data/lib/generators/fluxbit/devise_views_generator.rb +116 -0
  117. data/lib/generators/fluxbit/pagy_generator.rb +39 -0
  118. data/lib/generators/fluxbit/scaffold_generator.rb +165 -0
  119. data/lib/generators/fluxbit/templates/_alert.html.erb.tt +1 -0
  120. data/lib/generators/fluxbit/templates/_flash.html.erb.tt +15 -0
  121. data/lib/generators/fluxbit/templates/_form.html.erb.tt +38 -0
  122. data/lib/generators/fluxbit/templates/_metadata.html.erb.tt +44 -0
  123. data/lib/generators/fluxbit/templates/controller.rb.tt +406 -0
  124. data/lib/generators/fluxbit/templates/create.turbo_stream.erb.tt +7 -0
  125. data/lib/generators/fluxbit/templates/destroy.turbo_stream.erb.tt +3 -0
  126. data/lib/generators/fluxbit/templates/destroy_all.turbo_stream.erb.tt +9 -0
  127. data/lib/generators/fluxbit/templates/devise_views/confirmations/new.html.erb +11 -0
  128. data/lib/generators/fluxbit/templates/devise_views/layouts/devise.html.erb +64 -0
  129. data/lib/generators/fluxbit/templates/devise_views/mailer/confirmation_instructions.html.erb +5 -0
  130. data/lib/generators/fluxbit/templates/devise_views/mailer/email_changed.html.erb +7 -0
  131. data/lib/generators/fluxbit/templates/devise_views/mailer/password_changed.html.erb +3 -0
  132. data/lib/generators/fluxbit/templates/devise_views/mailer/reset_password_instructions.html.erb +8 -0
  133. data/lib/generators/fluxbit/templates/devise_views/mailer/unlock_instructions.html.erb +7 -0
  134. data/lib/generators/fluxbit/templates/devise_views/passwords/edit.html.erb +29 -0
  135. data/lib/generators/fluxbit/templates/devise_views/passwords/new.html.erb +11 -0
  136. data/lib/generators/fluxbit/templates/devise_views/registrations/edit.html.erb +43 -0
  137. data/lib/generators/fluxbit/templates/devise_views/registrations/new.html.erb +34 -0
  138. data/lib/generators/fluxbit/templates/devise_views/sessions/new.html.erb +15 -0
  139. data/lib/generators/fluxbit/templates/devise_views/shared/_error_messages.html.erb +14 -0
  140. data/lib/generators/fluxbit/templates/devise_views/shared/_links.html.erb +25 -0
  141. data/lib/generators/fluxbit/templates/devise_views/unlocks/new.html.erb +11 -0
  142. data/lib/generators/fluxbit/templates/edit.html.erb.tt +47 -0
  143. data/lib/generators/fluxbit/templates/fluxbit_pagy.css +27 -0
  144. data/lib/generators/fluxbit/templates/i18n.en.yml.tt +121 -0
  145. data/lib/generators/fluxbit/templates/i18n.pt-BR.yml.tt +121 -0
  146. data/lib/generators/fluxbit/templates/index.html.erb.tt +254 -0
  147. data/lib/generators/fluxbit/templates/index.json.jbuilder.tt +33 -0
  148. data/lib/generators/fluxbit/templates/new.html.erb.tt +47 -0
  149. data/lib/generators/fluxbit/templates/partial.html.erb.tt +61 -0
  150. data/lib/generators/fluxbit/templates/policy.rb.tt +36 -0
  151. data/lib/generators/fluxbit/templates/send_alert_via_drawer.erb.tt +10 -0
  152. data/lib/generators/fluxbit/templates/show.html.erb.tt +44 -0
  153. data/lib/generators/fluxbit/templates/show.json.jbuilder.tt +6 -0
  154. data/lib/generators/fluxbit/templates/update.turbo_stream.erb.tt +10 -0
  155. data/lib/generators/fluxbit/templates/update_all.turbo_stream.erb.tt +20 -0
  156. data/lib/install/install.rb +61 -3
  157. metadata +127 -35
  158. data/LICENSE.txt +0 -20
  159. data/app/components/fluxbit/form/checkbox_input_component.rb +0 -61
  160. data/app/components/fluxbit/form/datepicker_component.rb +0 -7
  161. data/app/components/fluxbit/form/radio_input_component.rb +0 -21
  162. data/app/components/fluxbit/form/range_input_component.rb +0 -51
  163. data/app/components/fluxbit/form/select_free_input_component.rb +0 -77
  164. data/app/components/fluxbit/form/select_input_component.rb +0 -21
  165. data/app/components/fluxbit/form/spacer_input_component.rb +0 -12
  166. data/app/components/fluxbit/form/text_input_component.rb +0 -225
  167. data/app/components/fluxbit/form/textarea_input_component.rb +0 -57
  168. data/app/components/fluxbit/form/toggle_input_component.rb +0 -166
  169. data/app/components/fluxbit/form/upload_image_input_component.html.erb +0 -48
  170. data/app/components/fluxbit/form/upload_image_input_component.rb +0 -61
  171. data/app/components/fluxbit/form/upload_input_component.html.erb +0 -12
  172. data/app/components/fluxbit/form/upload_input_component.rb +0 -47
  173. data/app/helpers/fluxbit/classes_helper.rb +0 -9
@@ -0,0 +1,50 @@
1
+ <%= content_tag :div, **@wrapper_html do %>
2
+ <div id="<%= id %>" class="mt-6 grow lg:mt-0 lg:ml-6 lg:shrink-0 lg:grow-0">
3
+ <%= label %>
4
+ <div class="mt-1 lg:hidden">
5
+ <div class="flex items-center">
6
+ <div class="inline-block h-12 w-12 shrink-0 overflow-hidden <%= container_rounded_class %> relative" aria-hidden="true">
7
+ <%= image_element %>
8
+ </div>
9
+ <div class="ml-5 rounded-md shadow-xs">
10
+ <div class="group relative flex items-center justify-center rounded-md border border-slate-300 py-2 px-3 focus-within:ring-2 focus-within:ring-sky-500 focus-within:ring-offset-2 hover:bg-slate-50">
11
+ <label for="mobile-<%= id %>" class="pointer-events-none relative text-sm font-medium leading-4 text-slate-700">
12
+ <span><%= @title %></span>
13
+ <span class="sr-only"><%= @label %></span>
14
+ </label>
15
+ <%= input_element(input_id: "mobile-#{id}") %>
16
+ </div>
17
+ </div>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="relative hidden overflow-hidden <%= container_rounded_class %> lg:block w-40">
22
+ <div class="inline-block h-40 w-40 shrink-0 overflow-hidden <%= container_rounded_class %> relative" aria-hidden="true">
23
+ <%= image_element %>
24
+ </div>
25
+ <label for="desktop-<%= id %>" class="absolute inset-0 flex flex-col h-full w-full items-center justify-center bg-blue-800/75 text-sm font-medium text-white opacity-0 hover:opacity-100">
26
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-white mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
27
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
28
+ </svg>
29
+ <span><%= @title %></span>
30
+ <span class="sr-only"><%= @label %></span>
31
+ <%= input_element(input_id: "desktop-#{id}") %>
32
+ </label>
33
+ </div>
34
+ <%= help_text %>
35
+ </div>
36
+
37
+ <script>
38
+ function loadFile(event, id) {
39
+ const images = document.querySelectorAll('.img_photo_' + id);
40
+ if (event.target.files && event.target.files[0]) {
41
+ images.forEach(function(img) {
42
+ img.src = URL.createObjectURL(event.target.files[0]);
43
+ img.onload = function() {
44
+ URL.revokeObjectURL(img.src);
45
+ }
46
+ });
47
+ }
48
+ }
49
+ </script>
50
+ <% end %>
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The `Fluxbit::Form::UploadImageComponent` renders a stylized image upload field with live preview,
4
+ # drag-and-drop UI, support for both mobile and desktop layouts, labels, helper text, and integration
5
+ # with Rails form builders and Active Storage attachments. It provides custom title/subtitle, placeholder,
6
+ # and image preview, and is fully configurable via props.
7
+ #
8
+ # @example Basic usage
9
+ # = render Fluxbit::Form::UploadImageComponent.new(attribute: :avatar, label: "Profile photo")
10
+ #
11
+ # @see docs/03_Forms/UploadImage.md For detailed documentation and examples.
12
+ class Fluxbit::Form::UploadImageComponent < Fluxbit::Form::FieldComponent
13
+ # Initializes the upload image component with the given properties.
14
+ #
15
+ # @param form [ActionView::Helpers::FormBuilder] The form builder (optional, for Rails forms)
16
+ # @param attribute [Symbol] The model attribute to be used in the form (required if using form builder)
17
+ # @param id [String] The id of the input element (optional)
18
+ # @param label [String] The label for the input field (optional)
19
+ # @param help_text [String] Additional help text for the input field (optional)
20
+ # @param helper_popover [String] Content for a popover helper (optional)
21
+ # @param helper_popover_placement [String] Placement of the popover (default: "right")
22
+ # @param image_path [String] Path to the image to be displayed (optional)
23
+ # @param image_placeholder [String] Placeholder image path if no image is attached (optional)
24
+ # @param title [Boolean, String] Whether to show a title (true for default, false to hide, or custom string)
25
+ # @param rounded [Boolean] Whether to show image as circle (true, default) or square with rounded edges (false)
26
+ # @param class [String] Additional CSS classes for the input element
27
+ # @param ... any other HTML attribute supported by file_field_tag
28
+ def initialize(**props)
29
+ super(**props)
30
+ @title = @props.delete(:title) || "Change"
31
+ @rounded = @props.delete(:rounded)
32
+ @rounded = true if @rounded.nil?
33
+ @image_path = @props.delete(:image_path) ||
34
+ (if @object&.send(@attribute).respond_to?(:attached?) && @object&.send(@attribute)&.send("attached?")
35
+ @object&.send(@attribute)&.variant(resize_to_fit: [ 160, 160 ])
36
+ end) || @props.delete(:image_placeholder) || ""
37
+
38
+ @props["class"] = "absolute inset-0 h-full w-full cursor-pointer rounded-md border-gray-300 opacity-0"
39
+ end
40
+
41
+ def input_element(input_id: nil)
42
+ @props["onchange"] = "loadFile(event, '#{id}')"
43
+ return file_field_tag @name, @props.merge(id: input_id || id) if @form.nil?
44
+
45
+ @form.file_field(@attribute, **@props, id: input_id || id)
46
+ end
47
+
48
+ def image_element
49
+ image_tag @image_path,
50
+ class: "img_photo_#{id} img_photo absolute inset-0 w-full h-full object-cover #{image_rounded_class}",
51
+ alt: @attribute&.to_s&.humanize
52
+ end
53
+
54
+ def container_rounded_class
55
+ @rounded ? "rounded-full" : "rounded-lg"
56
+ end
57
+
58
+ def image_rounded_class
59
+ @rounded ? "rounded-full" : "rounded-lg"
60
+ end
61
+ end
@@ -24,6 +24,7 @@ class Fluxbit::GravatarComponent < Fluxbit::AvatarComponent
24
24
  # @option props [Symbol] :filetype (:png) The filetype of the Gravatar (:png, :jpg, :gif).
25
25
  # @option props [Symbol] :default (:identicon) The default image to use if no Gravatar is found.
26
26
  # @option props [Integer] :size (:md) The size of the Gravatar base on the size provided by AvatarComponent.
27
+ # @option props [Boolean] :url_only (false) If true, returns only the Gravatar URL instead of rendering the avatar component.
27
28
  # @option props [String] :remove_class ('') Classes to be removed from the default Gravatar class list.
28
29
  # @option props [Hash] **props Remaining options declared as HTML attributes, applied to the Gravatar container.
29
30
  def initialize(**props)
@@ -37,10 +38,16 @@ class Fluxbit::GravatarComponent < Fluxbit::AvatarComponent
37
38
  }
38
39
  add class: gravatar_styles[:base], to: @props
39
40
  @email = @props.delete(:email)
41
+ @url_only = @props.delete(:url_only)
40
42
  src = gravatar_url
41
43
  super(src: src, **@props)
42
44
  end
43
45
 
46
+ def call
47
+ return gravatar_url.html_safe if @url_only
48
+ super
49
+ end
50
+
44
51
  # The raw MD5 hash of the users' email. Gravatar is particularly tricky as
45
52
  # it downcases all emails. This is really the guts of the module,
46
53
  # everything else is just convenience.
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fluxbit
4
+ module IconHelpers
5
+ def chevron_right(**props)
6
+ add to: props, class: "w-2.5 h-2.5", first_element: true
7
+ remove_class_from_props(props)
8
+ props["aria-hidden"] = "true"
9
+ props[:xmlns] = "http://www.w3.org/2000/svg"
10
+ props[:fill] = "none"
11
+ props[:viewBox] = "0 0 6 10"
12
+ stroke_width = props.delete(:stroke_width) || 2
13
+
14
+ tag.svg(**props) do
15
+ tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => stroke_width, d: "m1 9 4-4-4-4")
16
+ end
17
+ end
18
+
19
+ def chevron_left(**props)
20
+ add to: props, class: "w-2.5 h-2.5", first_element: true
21
+ remove_class_from_props(props)
22
+ props["aria-hidden"] = "true"
23
+ props[:xmlns] = "http://www.w3.org/2000/svg"
24
+ props[:fill] = "none"
25
+ props[:viewBox] = "0 0 6 10"
26
+ stroke_width = props.delete(:stroke_width) || 2
27
+
28
+ tag.svg(**props) do
29
+ tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => stroke_width, d: "m5 1-4 4 4 4")
30
+ end
31
+ end
32
+
33
+ def chevron_up(**props)
34
+ add to: props, class: "w-2.5 h-2.5", first_element: true
35
+ remove_class_from_props(props)
36
+ props["aria-hidden"] = "true"
37
+ props[:xmlns] = "http://www.w3.org/2000/svg"
38
+ props[:fill] = "none"
39
+ props[:viewBox] = "0 0 10 6"
40
+ stroke_width = props.delete(:stroke_width) || 2
41
+
42
+ tag.svg(**props) do
43
+ tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => stroke_width, d: "m1 5 4-4 4 4")
44
+ end
45
+ end
46
+
47
+ def chevron_down(**props)
48
+ add to: props, class: "w-2.5 h-2.5", first_element: true
49
+ remove_class_from_props(props)
50
+ props["aria-hidden"] = "true"
51
+ props[:xmlns] = "http://www.w3.org/2000/svg"
52
+ props[:fill] = "none"
53
+ props[:viewBox] = "0 0 10 6"
54
+ stroke_width = props.delete(:stroke_width) || 2
55
+
56
+ tag.svg(**props) do
57
+ tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => stroke_width, d: "m1 1 4 4 4-4")
58
+ end
59
+ end
60
+
61
+ def close_icon(**props)
62
+ props["aria-hidden"] = "true"
63
+ props[:xmlns] = "http://www.w3.org/2000/svg"
64
+ props[:fill] = "none"
65
+ props[:viewBox] = "0 0 14 14"
66
+
67
+ tag.svg(**props) do
68
+ tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => 2, d: "m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6")
69
+ end
70
+ end
71
+
72
+ def plus_icon(**props)
73
+ props["aria-hidden"] = "true"
74
+ props[:xmlns] = "http://www.w3.org/2000/svg"
75
+ props[:fill] = "currentColor"
76
+ props[:viewBox] = "0 0 24 24"
77
+
78
+ tag.svg(**props) do
79
+ tag.path("fill-rule" => "evenodd", d: "M12 3.75a.75.75 0 0 1 .75.75v6.75h6.75a.75.75 0 0 1 0 1.5h-6.75v6.75a.75.75 0 0 1-1.5 0v-6.75H4.5a.75.75 0 0 1 0-1.5h6.75V4.5a.75.75 0 0 1 .75-.75Z", "clip-rule" => "evenodd")
80
+ end
81
+ end
82
+
83
+ def chevron_double_left(**props)
84
+ add to: props, class: "w-3 h-2.5", first_element: true
85
+ remove_class_from_props(props)
86
+ props["aria-hidden"] = "true"
87
+ props[:xmlns] = "http://www.w3.org/2000/svg"
88
+ props[:fill] = "none"
89
+ props[:viewBox] = "0 0 10 10"
90
+ stroke_width = props.delete(:stroke_width) || 2
91
+
92
+ tag.svg(**props) do
93
+ safe_join [
94
+ tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => stroke_width, d: "M4 1l-4 4 4 4"),
95
+ tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => stroke_width, d: "M10 1l-4 4 4 4")
96
+ ]
97
+ end
98
+ end
99
+
100
+ def chevron_double_right(**props)
101
+ add to: props, class: "w-3 h-2.5", first_element: true
102
+ remove_class_from_props(props)
103
+ props["aria-hidden"] = "true"
104
+ props[:xmlns] = "http://www.w3.org/2000/svg"
105
+ props[:fill] = "none"
106
+ props[:viewBox] = "0 0 12 10"
107
+ stroke_width = props.delete(:stroke_width) || 2
108
+
109
+ tag.svg(**props) do
110
+ safe_join [
111
+ tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => stroke_width, d: "M2 1l4 4-4 4"),
112
+ tag.path(stroke: "currentColor", "stroke-linecap" => "round", "stroke-linejoin" => "round", "stroke-width" => stroke_width, d: "M8 1l4 4-4 4")
113
+ ]
114
+ end
115
+ end
116
+
117
+ def ellipsis_horizontal(**props)
118
+ add to: props, class: "w-2.5 h-2.5", first_element: true
119
+ remove_class_from_props(props)
120
+ props["aria-hidden"] = "true"
121
+ props[:xmlns] = "http://www.w3.org/2000/svg"
122
+ props[:fill] = "currentColor"
123
+ props[:viewBox] = "0 0 10 2"
124
+
125
+ tag.svg(**props) do
126
+ safe_join [
127
+ tag.circle(cx: 1, cy: 1, r: 1),
128
+ tag.circle(cx: 5, cy: 1, r: 1),
129
+ tag.circle(cx: 9, cy: 1, r: 1)
130
+ ]
131
+ end
132
+ end
133
+
134
+ def eye_icon(**props)
135
+ add to: props, class: "size-4", first_element: true
136
+ remove_class_from_props(props)
137
+ props["aria-hidden"] = "true"
138
+ props[:xmlns] = "http://www.w3.org/2000/svg"
139
+ props[:fill] = "none"
140
+ props[:viewBox] = "0 0 24 24"
141
+ props[:"stroke-width"] = "1.5"
142
+ props[:stroke] = "currentColor"
143
+
144
+ tag.svg(**props) do
145
+ safe_join [
146
+ tag.path("stroke-linecap" => "round", "stroke-linejoin" => "round", d: "M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"),
147
+ tag.path("stroke-linecap" => "round", "stroke-linejoin" => "round", d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z")
148
+ ]
149
+ end
150
+ end
151
+
152
+ def eye_slash_icon(**props)
153
+ add to: props, class: "size-4", first_element: true
154
+ remove_class_from_props(props)
155
+ props["aria-hidden"] = "true"
156
+ props[:xmlns] = "http://www.w3.org/2000/svg"
157
+ props[:fill] = "none"
158
+ props[:viewBox] = "0 0 24 24"
159
+ props[:"stroke-width"] = "1.5"
160
+ props[:stroke] = "currentColor"
161
+
162
+ tag.svg(**props) do
163
+ tag.path("stroke-linecap" => "round", "stroke-linejoin" => "round", d: "M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88")
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ ##
5
+ # The `Fluxbit::LinkComponent` is a customizable link component that extends {Fluxbit::Component}.
6
+ # It provides a straightforward way to generate anchor elements (`<a>`) with configurable colors
7
+ # and styling options. Additional HTML attributes can be passed to further control the component's
8
+ # behavior and appearance.
9
+ #
10
+ # Example usage:
11
+ # = render Fluxbit::LinkComponent.new(size: 2, spacing: :wider, line_height: :relaxed) do
12
+ # "My Heading"
13
+ #
14
+ class Fluxbit::LinkComponent < Fluxbit::Component
15
+ include Fluxbit::Config::LinkComponent
16
+
17
+ ##
18
+ # Initializes the link component with the provided options.
19
+ #
20
+ # @param [Symbol] color (:default) The color style for the heading. Must be one of the keys defined in +styles[:colors]+.
21
+ # @param [Hash] props Additional HTML attributes to be applied to the heading element, such as +class+, +id+, +data-*, etc.
22
+ #
23
+ # @return [Fluxbit::LinkComponent]
24
+ #
25
+ # @example
26
+ # = render Fluxbit::LinkComponent.new(color: :primary) do
27
+ # "My Heading"
28
+ #
29
+ def initialize(**props)
30
+ super
31
+ @props = props
32
+ @color = @props.delete(:color) || @@color
33
+
34
+ add to: @props, class: [ styles[:colors][@color], styles[:base] ]
35
+ @props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
36
+ @href = @props.delete(:href) || "#"
37
+ end
38
+
39
+ def call
40
+ link_to content, @href, **@props
41
+ end
42
+ end
@@ -23,10 +23,10 @@ class Fluxbit::ModalComponent < Fluxbit::Component
23
23
  # @option props [Boolean] :only_css (false) Determines if the modal can be closed by clicking the backdrop, using a CSS-based approach.
24
24
  # @option props [Boolean] :static (false) If true, the modal will not close when clicking the backdrop or pressing the ESC key.
25
25
  # @option props [String] :remove_class ('') Classes to be removed from the default modal class list.
26
- # @option props [Hash] :content_props ({}) Additional HTML attributes and classes for the content wrapper inside the modal.
27
- # @option props [Hash] :header_props ({}) Additional HTML attributes and classes for the header section.
28
- # @option props [Hash] :footer_props ({}) Additional HTML attributes and classes for the footer section.
29
- # @option props [Hash] :close_button_props ({}) Additional HTML attributes and classes for the close button element.
26
+ # @option props [Hash] :content_html ({}) Additional HTML attributes and classes for the content wrapper inside the modal.
27
+ # @option props [Hash] :header_html ({}) Additional HTML attributes and classes for the header section.
28
+ # @option props [Hash] :footer_html ({}) Additional HTML attributes and classes for the footer section.
29
+ # @option props [Hash] :close_button_html ({}) Additional HTML attributes and classes for the close button element.
30
30
  # @option props [Hash] **props Remaining options declared as HTML attributes, applied to the modal container.
31
31
  def initialize(**props)
32
32
  super
@@ -50,39 +50,35 @@ class Fluxbit::ModalComponent < Fluxbit::Component
50
50
  @props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
51
51
 
52
52
  # Content properties
53
- @content_props = @props.delete(:content_props) || {}
54
- add(class: content_classes, to: @content_props, first_element: true)
55
- @content_props[:class] = remove_class(@content_props.delete(:remove_class) || "", @content_props[:class])
53
+ @content_html = @props.delete(:content_html) || {}
54
+ add(class: content_classes, to: @content_html, first_element: true)
55
+ @content_html[:class] = remove_class(@content_html.delete(:remove_class) || "", @content_html[:class])
56
56
 
57
57
  # Header properties
58
- @header_props = @props.delete(:header_props) || {}
59
- add(class: header_classes, to: @header_props, first_element: true)
60
- @header_props[:class] = remove_class(@header_props.delete(:remove_class) || "", @header_props[:class])
58
+ @header_html = @props.delete(:header_html) || {}
59
+ add(class: header_classes, to: @header_html, first_element: true)
60
+ @header_html[:class] = remove_class(@header_html.delete(:remove_class) || "", @header_html[:class])
61
61
 
62
62
  # Footer properties
63
- @footer_props = @props.delete(:footer_props) || {}
64
- add(class: footer_classes, to: @footer_props, first_element: true)
65
- @footer_props[:class] = remove_class(@footer_props.delete(:remove_class) || "", @footer_props[:class])
63
+ @footer_html = @props.delete(:footer_html) || {}
64
+ add(class: footer_classes, to: @footer_html, first_element: true)
65
+ @footer_html[:class] = remove_class(@footer_html.delete(:remove_class) || "", @footer_html[:class])
66
66
 
67
67
  # Close button properties
68
- @close_button_props = @props.delete(:close_button_props) || {}
69
- add(class: styles[:header][:close][:base], to: @close_button_props, first_element: true)
70
- @close_button_props[:class] = remove_class(@close_button_props.delete(:remove_class) || "", @close_button_props[:class])
71
- @close_button_props[:type] = "button"
72
- @close_button_props["data-modal-hide"] = @props[:id]
73
- @close_button_props["aria-label"] = "Close"
68
+ @close_button_html = @props.delete(:close_button_html) || {}
69
+ add(class: styles[:header][:close][:base], to: @close_button_html, first_element: true)
70
+ @close_button_html[:class] = remove_class(@close_button_html.delete(:remove_class) || "", @close_button_html[:class])
71
+ @close_button_html[:type] = "button"
72
+ @close_button_html["data-modal-hide"] = @props[:id]
74
73
  end
75
74
 
76
75
  def call
77
- content_tag(
78
- :div,
79
- **@props
80
- ) do
81
- content_tag(:div, **@content_props) do
82
- content_tag(:div, class: styles[:content][:inner]) do
76
+ tag.div(**@props) do
77
+ tag.div(**@content_html) do
78
+ tag.div(class: styles[:content][:inner]) do
83
79
  concat(header) if title? || @title.present? || @close_button
84
- concat(content_tag(:div, content, class: body_classes))
85
- concat(content_tag(:div, footer, **@footer_props)) if footer?
80
+ concat(tag.div(content, class: body_classes))
81
+ concat(tag.div(footer, **@footer_html)) if footer?
86
82
  end
87
83
  end
88
84
  end
@@ -114,7 +110,7 @@ class Fluxbit::ModalComponent < Fluxbit::Component
114
110
  def header
115
111
  return close_button if @close_button && !title? && !@title.present?
116
112
 
117
- content_tag(:div, **@header_props) do
113
+ content_tag(:div, **@header_html) do
118
114
  concat(title) if title?
119
115
  concat(content_tag(:h3, @title, class: styles[:header][:title])) if @title.present?
120
116
  concat(close_button) if @close_button
@@ -126,12 +122,13 @@ class Fluxbit::ModalComponent < Fluxbit::Component
126
122
  end
127
123
 
128
124
  def close_button
125
+ @close_button_html["aria-label"] = t("fluxbit.modal.aria_close")
129
126
  content_tag(
130
127
  :button,
131
- **@close_button_props
128
+ **@close_button_html
132
129
  ) do
133
- concat content_tag(:span, "Dismiss", class: "sr-only")
134
- concat anyicon(icon: "heroicons_outline:x-mark", class: "w-5 h-5")
130
+ concat content_tag(:span, t("fluxbit.modal.dismiss"), class: "sr-only")
131
+ concat close_icon(class: "size-3")
135
132
  end
136
133
  end
137
134
 
@@ -0,0 +1,206 @@
1
+ # The `Fluxbit::PaginationComponent` is a component for rendering customizable pagination controls.
2
+ # It extends `Fluxbit::Component` and provides options for configuring the pagination's
3
+ # appearance, behavior, and content areas. You can control the pagination's layout, item count,
4
+ # and other interactive elements. The pagination is divided into different sections (previous, next, etc.),
5
+ # each of which can be styled or customized through various properties.
6
+ class Fluxbit::PaginationComponent < Fluxbit::Component
7
+ include Fluxbit::Config::PaginationComponent
8
+
9
+ def initialize(pagy = nil, **props)
10
+ @pagy = pagy
11
+
12
+ @props = props
13
+ @count = @props.delete(:count) || 0
14
+ @last = @props.delete(:last) || 1
15
+ @next = @props.delete(:next)
16
+ @page = @props.delete(:page) || 1
17
+ @prev = @props.delete(:prev)
18
+ @size = @props.delete(:size) || :default
19
+ @ends = @props.delete(:ends) || true
20
+ @request_path = @props.delete(:request_path) || nil
21
+
22
+ if @pagy
23
+ @count = @pagy.count
24
+ @last = @pagy.last
25
+ @next = @pagy.next
26
+ @page = @pagy.page
27
+ @prev = @pagy.prev
28
+ @size = @pagy.vars[:size]
29
+ @ends = @pagy.vars[:ends]
30
+ @request_path = @pagy.vars[:request_path]
31
+ end
32
+
33
+ unless @size.is_a?(Integer) && @size >= 0
34
+ raise ArgumentError, "expected :size to be an Integer >= 0, got #{@size.inspect} (#{@size.class})"
35
+ end
36
+
37
+ @show_first_last = options @props.delete(:show_first_last), default: @@show_first_last
38
+ @show_prev_next = options @props.delete(:show_prev_next), default: @@show_prev_next
39
+ @show_pages = options @props.delete(:show_pages), default: @@show_pages
40
+ @show_icons = options @props.delete(:show_icons), default: @@show_icons
41
+ @show_texts = options @props.delete(:show_texts), default: @@show_texts
42
+ @sizing = options @props.delete(:sizing), default: @@sizing
43
+ @aria_label = @props.delete(:aria_label) || translate("aria_label.nav", count: @last)
44
+ @show_texts = true if !@show_icons && !@show_texts
45
+
46
+ add(class: [ styles[:root], styles[:sizes][@sizing][:root] ], to: @props)
47
+ @page_link_style = [ styles[:page_link], styles[:sizes][@sizing][:page_link] ].join(" ")
48
+ @current_style = [ styles[:current], styles[:sizes][@sizing][:page_link] ].join(" ")
49
+ @props[:aria] ||= {}
50
+ @props[:aria][:label] = @aria_label unless @props[:aria][:label]
51
+ end
52
+
53
+ def call
54
+ tag.nav(**@props) do
55
+ concat first_button if @show_first_last
56
+ concat prev_button if @show_prev_next
57
+
58
+ if @show_pages
59
+ series.each do |item|
60
+ case item
61
+ when Integer
62
+ concat(tag.a(item.to_s, href: url_for(item), role: "link", class: @page_link_style, aria: { label: item.to_s }))
63
+ when String
64
+ concat(tag.a(item.to_s, role: "link", class: @current_style, aria: { disabled: true, current: "page" }))
65
+ when :gap
66
+ concat(tag.a(ellipsis_horizontal, role: "link", class: @page_link_style, aria: { disabled: true }))
67
+ end
68
+ end
69
+ end
70
+
71
+ concat next_button if @show_prev_next
72
+ concat last_button if @show_first_last
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def translate(key, options = {})
79
+ I18n.t(key, **options.merge(scope: "fluxbit.pagination")) # , default: Fluxbit::DEFAULT_TRANSLATIONS["fluxbit.pagination.#{key}"]))
80
+ end
81
+
82
+ def first_button
83
+ props = { role: "link", class: @page_link_style, aria: { label: translate("aria_label.first") } }
84
+ if @page != 1
85
+ props[:href] = url_for(1)
86
+ else
87
+ props[:aria][:disabled] = true
88
+ add class: styles[:disabled], to: props, first_element: true
89
+ end
90
+ add class: styles[:previous], to: props
91
+
92
+ tag.a(**props) do
93
+ concat(chevron_double_left) if @show_icons
94
+ concat(tag.span(
95
+ translate("first"),
96
+ class: @show_texts ? (@show_icons ? styles[:text_with_icon_prev] : styles[:only_text]) : styles[:only_icon]
97
+ ))
98
+ end
99
+ end
100
+
101
+ def last_button
102
+ props = { role: "link", class: @page_link_style, aria: { label: translate("aria_label.last") } }
103
+ if @page != @last
104
+ props[:href] = url_for(@last)
105
+ else
106
+ props[:aria][:disabled] = true
107
+ add class: styles[:disabled], to: props, first_element: true
108
+ end
109
+ add class: styles[:next], to: props
110
+
111
+ tag.a(**props) do
112
+ concat(tag.span(
113
+ translate("last"),
114
+ class: @show_texts ? (@show_icons ? styles[:text_with_icon_next] : styles[:only_text]) : styles[:only_icon]
115
+ ))
116
+ concat(chevron_double_right) if @show_icons
117
+ end
118
+ end
119
+
120
+ def prev_button
121
+ props = { role: "link", class: @page_link_style, aria: { label: translate("aria_label.prev") } }
122
+ if prev_page = @prev
123
+ props[:href] = url_for(prev_page)
124
+ else
125
+ props[:aria][:disabled] = true
126
+ add class: styles[:disabled], to: props, first_element: true
127
+ end
128
+ add(class: styles[:previous], to: props) unless @show_first_last
129
+
130
+ tag.a(**props) do
131
+ concat(chevron_left) if @show_icons
132
+ concat(tag.span(
133
+ translate("prev"),
134
+ class: @show_texts ? (@show_icons ? styles[:text_with_icon_prev] : styles[:only_text]) : styles[:only_icon]
135
+ )
136
+ )
137
+ end
138
+ end
139
+
140
+ def next_button
141
+ props = { role: "link", class: @page_link_style, aria: { label: translate("aria_label.next") } }
142
+ if next_page = @next
143
+ props[:href] = url_for(next_page)
144
+ else
145
+ props[:aria][:disabled] = true
146
+ add class: styles[:disabled], to: props, first_element: true
147
+ end
148
+ add(class: styles[:next], to: props) unless @show_first_last
149
+
150
+ tag.a(**props) do
151
+ concat(tag.span(
152
+ translate("next"),
153
+ class: @show_texts ? (@show_icons ? styles[:text_with_icon_next] : styles[:only_text]) : styles[:only_icon]
154
+ ))
155
+ concat(chevron_right) if @show_icons
156
+ end
157
+ end
158
+
159
+ def series
160
+ return @pagy.series(size: @size) if @pagy && @pagy.respond_to?(:series, true)
161
+ return [] if @size.zero?
162
+
163
+ [].tap do |series|
164
+ if @size >= @last
165
+ series.push(*1..@last)
166
+ else
167
+ left = ((@size - 1) / 2.0).floor # left half might be 1 page shorter for even size
168
+ start = if @page <= left # beginning pages
169
+ 1
170
+ elsif @page > (@last - @size + left) # end pages
171
+ @last - @size + 1
172
+ else # intermediate pages
173
+ @page - left
174
+ end
175
+ series.push(*start...start + @size)
176
+ # Set first and last pages plus gaps when needed, respecting the size
177
+ if @ends && @size >= 7
178
+ series[0] = 1
179
+ series[1] = :gap unless series[1] == 2
180
+ series[-2] = :gap unless series[-2] == @last - 1
181
+ series[-1] = @last
182
+ end
183
+ end
184
+ series[series.index(@page)] = @page.to_s
185
+ end
186
+ end
187
+
188
+ def url_for(page)
189
+ vars = @pagy&.vars || {}
190
+ # Use current request parameters as base
191
+ params = (respond_to?(:request) ? request.GET : controller.request.GET).dup
192
+ params.merge!(vars[:params].transform_keys(&:to_s)) if vars[:params].is_a?(Hash)
193
+ # Set page and possibly limit
194
+ params[vars[:page_param].to_s] = page
195
+ params[vars[:limit_param].to_s] = vars[:limit] if vars[:limit_extra]
196
+ # Apply params proc if given
197
+ params = vars[:params].call(params) if vars[:params].is_a?(Proc)
198
+
199
+ # Build query string
200
+ query_str = params.any? ? "?#{Rack::Utils.build_nested_query(params)}" : ""
201
+ # Base path (use stored request_path or current path)
202
+ base_path = @request_path || (respond_to?(:request) ? request.path : controller.request.path)
203
+ base_path = "#{request.base_url}#{base_path}" if vars[:absolute]
204
+ "#{base_path}#{query_str}#{vars[:fragment] || ''}"
205
+ end
206
+ end