fluxbit_view_components 0.1.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 (64) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +20 -0
  3. data/README.md +86 -0
  4. data/app/components/fluxbit/alert_component.rb +126 -0
  5. data/app/components/fluxbit/avatar_component.rb +113 -0
  6. data/app/components/fluxbit/avatar_group_component.rb +23 -0
  7. data/app/components/fluxbit/badge_component.rb +79 -0
  8. data/app/components/fluxbit/button_component.rb +97 -0
  9. data/app/components/fluxbit/button_group_component.rb +43 -0
  10. data/app/components/fluxbit/card_component.rb +135 -0
  11. data/app/components/fluxbit/component.rb +86 -0
  12. data/app/components/fluxbit/flex_component.rb +93 -0
  13. data/app/components/fluxbit/form/checkbox_input_component.rb +61 -0
  14. data/app/components/fluxbit/form/component.rb +71 -0
  15. data/app/components/fluxbit/form/datepicker_component.rb +7 -0
  16. data/app/components/fluxbit/form/form_builder_component.rb +117 -0
  17. data/app/components/fluxbit/form/helper_text_component.rb +29 -0
  18. data/app/components/fluxbit/form/label_component.rb +65 -0
  19. data/app/components/fluxbit/form/radio_input_component.rb +21 -0
  20. data/app/components/fluxbit/form/range_input_component.rb +51 -0
  21. data/app/components/fluxbit/form/select_free_input_component.rb +77 -0
  22. data/app/components/fluxbit/form/select_input_component.rb +21 -0
  23. data/app/components/fluxbit/form/spacer_input_component.rb +12 -0
  24. data/app/components/fluxbit/form/text_input_component.rb +225 -0
  25. data/app/components/fluxbit/form/textarea_input_component.rb +57 -0
  26. data/app/components/fluxbit/form/toggle_input_component.rb +166 -0
  27. data/app/components/fluxbit/form/upload_image_input_component.html.erb +48 -0
  28. data/app/components/fluxbit/form/upload_image_input_component.rb +66 -0
  29. data/app/components/fluxbit/form/upload_input_component.html.erb +12 -0
  30. data/app/components/fluxbit/form/upload_input_component.rb +47 -0
  31. data/app/components/fluxbit/gravatar_component.rb +99 -0
  32. data/app/components/fluxbit/heading_component.rb +47 -0
  33. data/app/components/fluxbit/modal_component.rb +141 -0
  34. data/app/components/fluxbit/popover_component.rb +71 -0
  35. data/app/components/fluxbit/tab_component.rb +142 -0
  36. data/app/components/fluxbit/text_component.rb +36 -0
  37. data/app/components/fluxbit/tooltip_component.rb +38 -0
  38. data/app/helpers/fluxbit/classes_helper.rb +21 -0
  39. data/app/helpers/fluxbit/components_helper.rb +75 -0
  40. data/config/deploy.yml +37 -0
  41. data/config/locales/en.yml +6 -0
  42. data/lib/fluxbit/config/alert_component.rb +59 -0
  43. data/lib/fluxbit/config/avatar_component.rb +79 -0
  44. data/lib/fluxbit/config/badge_component.rb +77 -0
  45. data/lib/fluxbit/config/button_component.rb +86 -0
  46. data/lib/fluxbit/config/card_component.rb +32 -0
  47. data/lib/fluxbit/config/flex_component.rb +63 -0
  48. data/lib/fluxbit/config/form/helper_text_component.rb +20 -0
  49. data/lib/fluxbit/config/gravatar_component.rb +19 -0
  50. data/lib/fluxbit/config/heading_component.rb +39 -0
  51. data/lib/fluxbit/config/modal_component.rb +71 -0
  52. data/lib/fluxbit/config/paragraph_component.rb +11 -0
  53. data/lib/fluxbit/config/popover_component.rb +33 -0
  54. data/lib/fluxbit/config/tab_component.rb +131 -0
  55. data/lib/fluxbit/config/text_component.rb +110 -0
  56. data/lib/fluxbit/config/tooltip_component.rb +11 -0
  57. data/lib/fluxbit/view_components/codemods/v3_slot_setters.rb +222 -0
  58. data/lib/fluxbit/view_components/engine.rb +36 -0
  59. data/lib/fluxbit/view_components/version.rb +7 -0
  60. data/lib/fluxbit/view_components.rb +30 -0
  61. data/lib/fluxbit_view_components.rb +3 -0
  62. data/lib/install/install.rb +64 -0
  63. data/lib/tasks/fluxbit_view_components_tasks.rake +22 -0
  64. metadata +238 -0
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fluxbit::Form::UploadImageInputComponent < Fluxbit::Form::Component
4
+ cattr_accessor :styles do
5
+ {
6
+ height: {
7
+ no: "",
8
+ sm: "h-32",
9
+ md: "h-64",
10
+ lg: "h-96"
11
+ }
12
+ }
13
+ end
14
+
15
+ def initialize(form: nil, field: nil, id: nil, height: :md, label: nil, helper_text: nil, helper_popover: nil,
16
+ helper_popover_placement: "right", type: nil, image_path: nil, image_placeholder: nil,
17
+ title: true, subtitle: true, **props)
18
+ super
19
+ @form = form
20
+ @object = form&.object
21
+ @field = field
22
+ @id = id
23
+ @title = title
24
+ @subtitle = subtitle
25
+ @props = props
26
+ # @height = height.in?(styles[:height].keys) ? height : :md
27
+ @label = label_value(label, @object, field, id)
28
+ @helper_text = define_helper_text(helper_text, @object, field)
29
+ @helper_popover = define_helper_popover(helper_popover, @object, field)
30
+ @helper_popover_placement = helper_popover_placement
31
+ # binding.pry
32
+ @image_path = image_path || (if @object&.send(@field)&.send("attached?")
33
+ @object&.send(@field)&.variant(
34
+ resize_to_fit: [
35
+ 160, 160
36
+ ]
37
+ )
38
+ end) || image_placeholder
39
+ @props["class"] = "absolute inset-0 h-full w-full cursor-pointer rounded-md border-gray-300 opacity-0"
40
+ end
41
+
42
+ def input_element(input_id: nil)
43
+ @props["onchange"] = "loadFile(event, '#{id}')"
44
+ return content_tag :input, nil, @props.merge(id: input_id || id) if @form.nil?
45
+
46
+ @form.file_field(@field, **@props, id: input_id || id)
47
+ end
48
+
49
+ def image_element
50
+ image_tag @image_path,
51
+ class: "img_photo absolute inset-0 w-full h-full object-cover rounded-full",
52
+ alt: @field&.to_s&.humanize
53
+ end
54
+
55
+ def title
56
+ return safe_join(content_tag(:span, "Click to upload", class: "font-semibold"), " or drag and drop") if @title == true
57
+
58
+ @title
59
+ end
60
+
61
+ def subtitle
62
+ return "SVG, PNG, JPG or GIF (MAX. 800x400px)" if @subtitle == true
63
+
64
+ @subtitle
65
+ end
66
+ end
@@ -0,0 +1,12 @@
1
+ <div class="<%= self.styles[:bae] %>">
2
+ <label for="<%= id %>" class="<%= self.styles[:label] %> <%= self.styles[:height][@height] %>">
3
+ <div class="<%= self.styles[:inner_div] %>">
4
+ <svg class="w-8 h-8 mb-4 text-slate-500 dark:text-slate-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
5
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/>
6
+ </svg>
7
+ <p class="<%= self.styles[:title] %>"><%= title %></p>
8
+ <p class="<%= self.styles[:subtitle] %>"><%= subtitle %></p>
9
+ </div>
10
+ <input id="<%= id %>" name="<%= @name %>" type="file" class="hidden" />
11
+ </label>
12
+ </div>
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fluxbit::Form::UploadInputComponent < Fluxbit::Form::Component
4
+ # rubocop: disable Layout/LineLength
5
+ cattr_accessor :styles do
6
+ {
7
+ base: "flex items-center justify-center w-full",
8
+ label: "flex flex-col items-center justify-center w-full border-2 border-slate-300 border-dashed rounded-lg cursor-pointer bg-slate-50 dark:hover:bg-bray-800 dark:bg-slate-700 hover:bg-slate-100 dark:border-slate-600 dark:hover:border-slate-500 dark:hover:bg-slate-600",
9
+ inner_div: "flex flex-col items-center justify-center pt-5 pb-6",
10
+ title: "mb-2 text-sm text-slate-500 dark:text-slate-400",
11
+ subtitle: "text-xs text-slate-500 dark:text-slate-400",
12
+ height: {
13
+ no: "",
14
+ sm: "h-32",
15
+ md: "h-64",
16
+ lg: "h-96"
17
+ }
18
+ }
19
+ end
20
+ # rubocop: enable Layout/LineLength
21
+
22
+ def initialize(form: nil, field: nil, id: nil, height: :md,
23
+ title: true, subtitle: true, **props)
24
+ super
25
+ @form = form
26
+ @object = form&.object
27
+ @field = field
28
+ @id = id
29
+ @title = title
30
+ @subtitle = subtitle
31
+ @props = props
32
+ @height = height.in?(styles[:height].keys) ? height : :md
33
+ end
34
+
35
+ def title
36
+ return safe_join(content_tag(:span, "Click to upload", class: "font-semibold"),
37
+ " or drag and drop") if @title == true
38
+
39
+ @title
40
+ end
41
+
42
+ def subtitle
43
+ return "SVG, PNG, JPG or GIF (MAX. 800x400px)" if @subtitle == true
44
+
45
+ @subtitle
46
+ end
47
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ # From:
4
+ # https://github.com/chrislloyd/gravtastic/blob/master/lib/gravtastic.rb
5
+ # https://chrislloyd.github.io/gravtastic/
6
+
7
+ require "digest/md5"
8
+
9
+ # The `Fluxbit::GravatarComponent` is a component for rendering Gravatar avatars.
10
+ # It extends `Fluxbit::AvatarComponent` and provides options for configuring the
11
+ # Gravatar's appearance and behavior. You can control the Gravatar's rating, size,
12
+ # filetype, and other attributes. The Gravatar URL is constructed based on the
13
+ # provided email address and options.
14
+ class Fluxbit::GravatarComponent < Fluxbit::AvatarComponent
15
+ include Fluxbit::Config::AvatarComponent
16
+ include Fluxbit::Config::GravatarComponent
17
+
18
+ # Initializes the Gravatar component with the given properties.
19
+ #
20
+ # @param [Hash] props The properties to customize the Gravatar.
21
+ # @option props [String] :email The email address associated with the Gravatar.
22
+ # @option props [Symbol] :rating (:g) The rating of the Gravatar (:g, :pg, :r, :x).
23
+ # @option props [Boolean] :secure (true) Whether to use HTTPS for the Gravatar URL.
24
+ # @option props [Symbol] :filetype (:png) The filetype of the Gravatar (:png, :jpg, :gif).
25
+ # @option props [Symbol] :default (:identicon) The default image to use if no Gravatar is found.
26
+ # @option props [Integer] :size (:md) The size of the Gravatar base on the size provided by AvatarComponent.
27
+ # @option props [String] :remove_class ('') Classes to be removed from the default Gravatar class list.
28
+ # @option props [Hash] **props Remaining options declared as HTML attributes, applied to the Gravatar container.
29
+ def initialize(**props)
30
+ @props = props
31
+ @gravatar_options = {
32
+ rating: options((@props.delete(:rating)|| "").to_sym, collection: gravatar_styles[:rating], default: @@rating),
33
+ secure: options(@props.delete(:secure), default: true),
34
+ filetype: options((@props.delete(:filetype)|| "").to_sym, collection: gravatar_styles[:filetype], default: @@filetype),
35
+ default: options((@props.delete(:default)|| "").to_sym, collection: gravatar_styles[:default], default: @@default),
36
+ size: gravatar_styles[:size][options(@props[:size], collection: gravatar_styles[:size], default: @@size)]
37
+ }
38
+ add class: gravatar_styles[:base], to: @props
39
+ @email = @props.delete(:email)
40
+ src = gravatar_url
41
+ super(src: src, **@props)
42
+ end
43
+
44
+ # The raw MD5 hash of the users' email. Gravatar is particularly tricky as
45
+ # it downcases all emails. This is really the guts of the module,
46
+ # everything else is just convenience.
47
+ def gravatar_id
48
+ Digest::MD5.hexdigest(@email.to_s.downcase)
49
+ end
50
+
51
+ # Constructs the full Gravatar url.
52
+ def gravatar_url
53
+ gravatar_hostname(@gravatar_options.delete(:secure)) +
54
+ gravatar_filename(@gravatar_options.delete(:filetype)) +
55
+ "?#{url_params_from_hash(process_options(@gravatar_options))}"
56
+ end
57
+
58
+ # Creates a params hash like "?foo=bar" from a hash like {'foo' => 'bar'}.
59
+ # The values are sorted so it produces deterministic output (and can
60
+ # therefore be tested easily).
61
+ def url_params_from_hash(hash)
62
+ hash.map do |key, val|
63
+ [ gravatar_abbreviations[key.to_sym] || key.to_s, val.to_s ].join("=")
64
+ end.sort.join("&")
65
+ end
66
+
67
+ # Returns either Gravatar's secure hostname or not.
68
+ def gravatar_hostname(secure)
69
+ "http#{secure ? 's://secure.' : '://'}gravatar.com/avatar/"
70
+ end
71
+
72
+ # Munges the ID and the filetype into one. Like "abc123.png"
73
+ def gravatar_filename(filetype)
74
+ "#{gravatar_id}.#{filetype}"
75
+ end
76
+
77
+ # Some options need to be processed before becoming URL params
78
+ def process_options(options_to)
79
+ processed_options = {}
80
+ options_to.each do |key, val|
81
+ case key
82
+ when :forcedefault
83
+ processed_options[key] = "y" if val
84
+ else
85
+ processed_options[key] = val
86
+ end
87
+ end
88
+ processed_options
89
+ end
90
+
91
+ def gravatar_abbreviations
92
+ {
93
+ size: "s",
94
+ default: "d",
95
+ rating: "r",
96
+ forcedefault: "f"
97
+ }
98
+ end
99
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The `Fluxbit::HeadingComponent` is a customizable heading component that extends {Fluxbit::Component}.
5
+ # It provides a straightforward way to generate heading elements (`<h1>` through `<h6>`) with
6
+ # configurable sizes, letter spacing, and line heights. Additional HTML attributes can be
7
+ # passed to further control the component’s behavior and styling.
8
+ #
9
+ # Example usage:
10
+ # = render Fluxbit::HeadingComponent.new(size: 2, spacing: :wider, line_height: :relaxed) do
11
+ # "My Heading"
12
+ #
13
+ class Fluxbit::HeadingComponent < Fluxbit::Component
14
+ include Fluxbit::Config::HeadingComponent
15
+
16
+ ##
17
+ # Initializes the heading component with the provided options.
18
+ #
19
+ # @param [Integer] size (1) The heading level (1 through 6). Determines whether the component is `<h1>`, `<h2>`, etc.
20
+ # @param [Symbol] spacing (:tight) The letter spacing style for the heading. Must be one of the keys defined in +styles[:spacings]+.
21
+ # @param [Symbol] line_height (:none) The line height style for the heading. Must be one of the keys defined in +styles[:line_heights]+.
22
+ # @param [Hash] props Additional HTML attributes to be applied to the heading element, such as +class+, +id+, +data-*, etc.
23
+ #
24
+ # @return [Fluxbit::HeadingComponent]
25
+ #
26
+ # @example
27
+ # = render Fluxbit::HeadingComponent.new(size: 2, spacing: :wider, line_height: :relaxed) do
28
+ # "My Heading"
29
+ #
30
+ def initialize(size: nil, spacing: nil, line_height: nil, **props)
31
+ super
32
+ @props = props
33
+ @base_component = "h#{size || @@size}".to_sym
34
+
35
+ add to: @props, class: [
36
+ styles[:base],
37
+ styles[:sizes][@base_component],
38
+ styles[:spacings][spacing || @@spacing],
39
+ styles[:line_heights][line_height || @@line_height]
40
+ ]
41
+ @props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
42
+ end
43
+
44
+ def call
45
+ content_tag @base_component, content, **@props
46
+ end
47
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The `Fluxbit::ModalComponent` is a component for rendering customizable modals.
4
+ # It extends `Fluxbit::Component` and provides options for configuring the modal's
5
+ # appearance, behavior, and content areas. You can control the modal's title, size,
6
+ # placement, backdrop behavior, and other interactive elements. The modal is divided
7
+ # into different sections (header, content, and footer), each of which can be
8
+ # styled or customized through various properties.
9
+ class Fluxbit::ModalComponent < Fluxbit::Component
10
+ include Fluxbit::Config::ModalComponent
11
+ renders_one :title
12
+ renders_one :footer
13
+
14
+ # Initializes the modal component with the given properties.
15
+ #
16
+ # @param [Hash] props The properties to customize the modal.
17
+ # @option props [String] :title (nil) The title text displayed in the modal header.
18
+ # @option props [Boolean] :opened (false) Determines if the modal is initially open (visible).
19
+ # @option props [Boolean] :close_button (true) Determines if a close button should be displayed in the header.
20
+ # @option props [Boolean] :flat (false) Applies a "flat" style (implementation-defined).
21
+ # @option props [Symbol, Integer] :size (1) The size of the modal (e.g., 0 to 9).
22
+ # @option props [Symbol, String] :placement (nil) The placement of the modal (e.g., :center, :top, :bottom).
23
+ # @option props [Boolean] :only_css (false) Determines if the modal can be closed by clicking the backdrop, using a CSS-based approach.
24
+ # @option props [Boolean] :static (false) If true, the modal will not close when clicking the backdrop or pressing the ESC key.
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.
30
+ # @option props [Hash] **props Remaining options declared as HTML attributes, applied to the modal container.
31
+ def initialize(**props)
32
+ super
33
+
34
+ # Main properties
35
+ @props = props
36
+ @title = @props.delete(:title)
37
+ @opened = options(@props.delete(:opened), default: @@opened)
38
+ @close_button = options(@props.delete(:close_button), default: @@close_button)
39
+ @flat = options(@props.delete(:flat), default: @@flat)
40
+ @size = options(@props.delete(:size), default: @@size)
41
+ @placement = options(@props.delete(:placement), default: @@placement)
42
+ @only_css = options(@props.delete(:only_css), default: @@only_css)
43
+ @static = options(@props.delete(:static), default: @@static)
44
+
45
+ add(class: modal_classes, to: @props, first_element: true)
46
+ @props["data-modal-placement"] = @placement.to_s if @placement
47
+ @props["aria-hidden"] = !@opened
48
+ @props["data-modal-backdrop"] = "static" if @static
49
+ @props["onclick"] = "if(event.target === this) this.classList.add('hidden')" if @only_css && !@static
50
+ @props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
51
+
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])
56
+
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])
61
+
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])
66
+
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"
74
+ end
75
+
76
+ 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
83
+ 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?
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def content_classes
94
+ [ styles[:content][:base], styles[:root][:size][@size] ].join(" ")
95
+ end
96
+
97
+ def modal_classes
98
+ [
99
+ styles[:root][:base],
100
+ styles[:root][:show][@opened ? :on : :off],
101
+ (@only_css ? styles[:root][:backdrop] : ""),
102
+ (@only_css ? styles[:root][:placements][@placement || :center] : "")
103
+ ].join(" ")
104
+ end
105
+
106
+ def body_classes
107
+ [
108
+ styles[:body][:base],
109
+ (@flat ? styles[:body][:flat] : nil),
110
+ (@close_button && !title? && !@title.present? ? styles[:body][:no_title] : "")
111
+ ].compact.join(" ")
112
+ end
113
+
114
+ def header
115
+ return close_button if @close_button && !title? && !@title.present?
116
+
117
+ content_tag(:div, **@header_props) do
118
+ concat(title) if title?
119
+ concat(content_tag(:h3, @title, class: styles[:header][:title])) if @title.present?
120
+ concat(close_button) if @close_button
121
+ end
122
+ end
123
+
124
+ def header_classes
125
+ [ styles[:header][:base], (@flat ? styles[:header][:flat] : nil) ].compact.join(" ")
126
+ end
127
+
128
+ def close_button
129
+ content_tag(
130
+ :button,
131
+ **@close_button_props
132
+ ) do
133
+ concat content_tag(:span, "Dismiss", class: "sr-only")
134
+ concat anyicon(icon: "heroicons_outline:x-mark", class: "w-5 h-5")
135
+ end
136
+ end
137
+
138
+ def footer_classes
139
+ [ styles[:footer][:base], (@flat ? styles[:footer][:flat] : nil) ].compact.join(" ")
140
+ end
141
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The `Fluxbit::PopoverComponent` is a component for rendering customizable popovers.
4
+ # It extends `Fluxbit::Component` and provides options for configuring the popover's
5
+ # appearance, behavior, and content areas. You can control the popover's trigger,
6
+ # placement, and other interactive elements. The popover is divided into different
7
+ # sections (trigger and content), each of which can be styled or customized through
8
+ # various properties.
9
+ class Fluxbit::PopoverComponent < Fluxbit::Component
10
+ include Fluxbit::Config::PopoverComponent
11
+
12
+ # Initializes the popover component with the given properties.
13
+ #
14
+ # @param [Hash] props The properties to customize the popover.
15
+ # @option props [String] :title (nil) The title text displayed in the popover.
16
+ # @option props [Boolean] :has_arrow (true) Determines if an arrow should be displayed on the popover.
17
+ # @option props [String] :image (nil) The URL of an image to be displayed in the popover.
18
+ # @option props [Symbol] :image_position (:right) The position of the image relative to the content (:left or :right).
19
+ # @option props [Hash] :image_props ({}) Additional HTML attributes for the image element.
20
+ # @option props [Symbol, String] :size (2) The size of the popover (0 to 4).
21
+ # @option props [String] :remove_class ('') Classes to be removed from the default popover class list.
22
+ # @option props [Hash] **props Remaining options declared as HTML attributes, applied to the popover container.
23
+ def initialize(**props)
24
+ super
25
+ @props = props
26
+ @title = @props.delete(:title)
27
+ @has_arrow = options @props.delete(:has_arrow), default: @@has_arrow
28
+ @image = @props.delete(:image)
29
+ @image_position = options @props.delete(:image_position), default: @@image_position
30
+ @image_props = options @props.delete(:image_props), default: @@image_props
31
+ @props["data-popover"] = "data-popover"
32
+ @props["role"] = "tooltip"
33
+
34
+ add(class: [ styles[:base], styles[:size][@props.delete(:size) || @@size] ], to: @props)
35
+ add(class: styles[:image_content][:image], to: @image_props)
36
+ @image_props[:src] = @image
37
+
38
+ @props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
39
+ @image_props[:class] = remove_class(@props.delete(:remove_class) || "", @image_props[:class])
40
+ end
41
+
42
+ def call
43
+ content_tag :div, @props do
44
+ concat div_title unless @title.blank?
45
+ concat (content_tag(:div, class: styles[@image.blank? ? :content : :image_base]) do
46
+ if @image.blank?
47
+ content
48
+ else
49
+ if @image_position == :left
50
+ concat content_tag(:img, nil, @image_props)
51
+ concat content_tag(:div, content, class: styles[:image_content][:text])
52
+ else
53
+ concat content_tag(:div, content, class: styles[:image_content][:text])
54
+ concat content_tag(:img, nil, @image_props)
55
+ end
56
+ end
57
+ end)
58
+ concat popper_arrow if @has_arrow
59
+ end
60
+ end
61
+
62
+ def popper_arrow
63
+ content_tag :div, "", "data-popper-arrow" => true
64
+ end
65
+
66
+ def div_title
67
+ content_tag :div, class: styles[:title][:div] do
68
+ content_tag :h3, @title, class: styles[:title][:h3]
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fluxbit::TabComponent < Fluxbit::Component
4
+ include Fluxbit::Config::TabComponent
5
+
6
+ attr_reader :variant, :lazy_load
7
+
8
+ renders_many :tabs, lambda { |**props, &block|
9
+ begin
10
+ @tabs_group << ComponentObj.new(props, view_context.capture(&block))
11
+ rescue
12
+ @tabs_group << ComponentObj.new(props, nil)
13
+ end
14
+ }
15
+
16
+ def initialize(**props)
17
+ @variant = (props.delete(:variant) || @@variant).to_sym
18
+ @color = props.delete(:color) || @@color
19
+ @vertical = props.delete(:vertical) || @@vertical
20
+ @tab_panel = (props.delete(:tab_panel) || @@tab_panel).to_sym
21
+ @tabs_group = []
22
+ @ul_props = props.delete(:ul_props) || {}
23
+ @props = props
24
+ @vertical = false if @variant == :full_width
25
+ super
26
+ end
27
+
28
+ def call
29
+ tabs
30
+ @has_panels = @tabs_group.map(&:content).compact.present?
31
+ add class: styles[:div][@vertical ? :vertical : :horizontal], to: @props, first_element: true
32
+
33
+ if @has_panels
34
+ content_tag :div, **@props do
35
+ concat(render_tab_list)
36
+ concat(render_tab_panels)
37
+ end
38
+ else
39
+ concat(render_tab_list)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def render_tab_list
46
+ add class: styles[:tab_list][:ul][@vertical ? :vertical : :horizontal], to: @ul_props, first_element: true
47
+ add class: styles[:tab_list][:variant][variant], to: @ul_props
48
+ @ul_props[:role] = "tablist"
49
+
50
+ if @has_panels
51
+ @ul_props[:data] = {
52
+ "tabs-toggle": "##{fx_id}-content",
53
+ "tabs-active-classes": styles[:tab_list][:tab_item][:variant][variant][:active][@color],
54
+ "tabs-inactive-classes": styles[:tab_list][:tab_item][:variant][variant][:inactive]
55
+ }
56
+ end
57
+
58
+ @ul_props[:id] = fx_id
59
+
60
+ content_tag :ul, **@ul_props do
61
+ safe_join(@tabs_group.map.with_index { |tab, index| render_tab(tab, index) })
62
+ end
63
+ end
64
+
65
+ def variant_color_style(active, disabled)
66
+ if active
67
+ styles[:tab_list][:tab_item][:variant][variant][:active][@color]
68
+ elsif disabled
69
+ styles[:tab_list][:tab_item][:variant][variant][:disabled]
70
+ else
71
+ styles[:tab_list][:tab_item][:variant][variant][:inactive]
72
+ end
73
+ end
74
+
75
+ def render_tab(tab, index)
76
+ tab_icon = tab.props.delete(:icon)
77
+ tab_title = tab.props.delete(:title)
78
+ tab_active = tab.props.delete(:active)
79
+
80
+ add class: [
81
+ styles[:tab_list][:tab_item][:base],
82
+ styles[:tab_list][:tab_item][:variant][variant][:base],
83
+ variant_color_style(tab_active, tab.props[:disabled])
84
+ ], to: tab.props, first_element: true
85
+
86
+ add(class: styles[:tab_list][:tab_item][:variant][variant][:first], to: tab.props) if index.zero? && styles[:tab_list][:tab_item][:variant][variant][:first].present?
87
+ add(class: styles[:tab_list][:tab_item][:variant][variant][:last], to: tab.props) if index == @tabs_group.size - 1 && styles[:tab_list][:tab_item][:variant][variant][:last].present?
88
+ add(class: styles[:tab_list][:tab_item][:variant][variant][:middle], to: tab.props) if index > 0 && index < @tabs_group.size - 1 && styles[:tab_list][:tab_item][:variant][variant][:middle].present?
89
+
90
+ tab.props[:role] = "tab"
91
+ tab.props[:"aria-selected"] = tab.props[:active].to_s
92
+ tab.props[:"aria-controls"] = "#{fx_id}-tabpanel-#{index}"
93
+ tab.props[:id] = "#{fx_id}-#{index}"
94
+ tab.props[:type] = "button"
95
+ tab.props[:data] = { "tabs-target": "##{fx_id}-tabpanel-#{index}" }
96
+ tab.props[:href] = "#" if tab.props[:href].blank?
97
+
98
+ if tab.props[:disabled].present? && tab.props[:disabled]
99
+ tab.props.delete :href
100
+ tab.props.delete :role
101
+ tab.props.delete :"aria-selected"
102
+ tab.props.delete :"aria-controls"
103
+ end
104
+
105
+ li_props = tab.props.delete(:li_props) || {}
106
+ li_props[:role] = "presentation"
107
+ li_props[:id] = "#{fx_id}-#{index}-li"
108
+ add class: styles[:tab_list][:li], to: li_props, first_element: true
109
+
110
+ content_tag :li, **li_props do
111
+ content_tag :a, **tab.props do
112
+ concat(render_icon(tab_icon)) if tab_icon
113
+ concat(content_tag(:span, tab_title))
114
+ end
115
+ end
116
+ end
117
+
118
+ def render_icon(icon)
119
+ if icon.include?('class="')
120
+ icon.gsub("class=\"", "class=\"#{styles[:tab_list][:tab_item][:icon]} ")
121
+ else
122
+ icon.gsub("<svg", "<svg class=\"#{styles[:tab_list][:tab_item][:icon]}\"")
123
+ end.html_safe
124
+ end
125
+
126
+ def render_tab_panels
127
+ content_tag :div, id: "##{fx_id}-content", class: styles[:tabpanel_container][@vertical ? :vertical : :horizontal] do
128
+ safe_join(@tabs_group.map.with_index { |tab, index| render_tabpanel(tab, index) })
129
+ end
130
+ end
131
+
132
+ def render_tabpanel(tab, index)
133
+ content_props = tab.props[:content_props] || {}
134
+ add class: styles[:tabpanel][@vertical ? :vertical : :horizontal][@tab_panel][tab.props[:active] ? :active : :inactive], to: content_props, first_element: true
135
+
136
+ content_props[:id] = "#{fx_id}-tabpanel-#{index}"
137
+ content_props[:role] = "tabpanel"
138
+ content_props[:"aria-labelledby"] = "#{fx_id}-#{index}"
139
+
140
+ content_tag :div, tab.content, **content_props
141
+ end
142
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The `Fluxbit::TextComponent` is a component for rendering customizable text elements.
4
+ # It extends `Fluxbit::Component` and provides options for configuring the text's
5
+ # appearance and behavior. You can control the text's tag, styles, and other attributes.
6
+ # The text can be rendered as different HTML tags (e.g., span, div) and can have various
7
+ # styles applied based on the provided properties.
8
+ class Fluxbit::TextComponent < Fluxbit::Component
9
+ include Fluxbit::Config::TextComponent
10
+
11
+ # Initializes the text component with the given properties.
12
+ #
13
+ # @param [Hash] props The properties to customize the text.
14
+ # @option props [Symbol] :as (:span) The HTML tag to use for the text element.
15
+ # @option props [Hash] **props Remaining options declared as HTML attributes, applied to the text element.
16
+ def initialize(**props)
17
+ super
18
+ @props = props.compact
19
+ @as = @props.delete(:as) || :span
20
+
21
+ styles.each do |style_type, style_values|
22
+ if @props.key?(style_type)
23
+ element = @props.delete(style_type)
24
+ if style_values.is_a?(Array)
25
+ add(class: style_values[element.to_i], to: @props)
26
+ else
27
+ add(class: style_values[element.to_sym], to: @props) if style_values.key?(element.to_sym)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def call
34
+ content_tag @as, content, @props
35
+ end
36
+ end