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.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +86 -0
- data/app/components/fluxbit/alert_component.rb +126 -0
- data/app/components/fluxbit/avatar_component.rb +113 -0
- data/app/components/fluxbit/avatar_group_component.rb +23 -0
- data/app/components/fluxbit/badge_component.rb +79 -0
- data/app/components/fluxbit/button_component.rb +97 -0
- data/app/components/fluxbit/button_group_component.rb +43 -0
- data/app/components/fluxbit/card_component.rb +135 -0
- data/app/components/fluxbit/component.rb +86 -0
- data/app/components/fluxbit/flex_component.rb +93 -0
- data/app/components/fluxbit/form/checkbox_input_component.rb +61 -0
- data/app/components/fluxbit/form/component.rb +71 -0
- data/app/components/fluxbit/form/datepicker_component.rb +7 -0
- data/app/components/fluxbit/form/form_builder_component.rb +117 -0
- data/app/components/fluxbit/form/helper_text_component.rb +29 -0
- data/app/components/fluxbit/form/label_component.rb +65 -0
- data/app/components/fluxbit/form/radio_input_component.rb +21 -0
- data/app/components/fluxbit/form/range_input_component.rb +51 -0
- data/app/components/fluxbit/form/select_free_input_component.rb +77 -0
- data/app/components/fluxbit/form/select_input_component.rb +21 -0
- data/app/components/fluxbit/form/spacer_input_component.rb +12 -0
- data/app/components/fluxbit/form/text_input_component.rb +225 -0
- data/app/components/fluxbit/form/textarea_input_component.rb +57 -0
- data/app/components/fluxbit/form/toggle_input_component.rb +166 -0
- data/app/components/fluxbit/form/upload_image_input_component.html.erb +48 -0
- data/app/components/fluxbit/form/upload_image_input_component.rb +66 -0
- data/app/components/fluxbit/form/upload_input_component.html.erb +12 -0
- data/app/components/fluxbit/form/upload_input_component.rb +47 -0
- data/app/components/fluxbit/gravatar_component.rb +99 -0
- data/app/components/fluxbit/heading_component.rb +47 -0
- data/app/components/fluxbit/modal_component.rb +141 -0
- data/app/components/fluxbit/popover_component.rb +71 -0
- data/app/components/fluxbit/tab_component.rb +142 -0
- data/app/components/fluxbit/text_component.rb +36 -0
- data/app/components/fluxbit/tooltip_component.rb +38 -0
- data/app/helpers/fluxbit/classes_helper.rb +21 -0
- data/app/helpers/fluxbit/components_helper.rb +75 -0
- data/config/deploy.yml +37 -0
- data/config/locales/en.yml +6 -0
- data/lib/fluxbit/config/alert_component.rb +59 -0
- data/lib/fluxbit/config/avatar_component.rb +79 -0
- data/lib/fluxbit/config/badge_component.rb +77 -0
- data/lib/fluxbit/config/button_component.rb +86 -0
- data/lib/fluxbit/config/card_component.rb +32 -0
- data/lib/fluxbit/config/flex_component.rb +63 -0
- data/lib/fluxbit/config/form/helper_text_component.rb +20 -0
- data/lib/fluxbit/config/gravatar_component.rb +19 -0
- data/lib/fluxbit/config/heading_component.rb +39 -0
- data/lib/fluxbit/config/modal_component.rb +71 -0
- data/lib/fluxbit/config/paragraph_component.rb +11 -0
- data/lib/fluxbit/config/popover_component.rb +33 -0
- data/lib/fluxbit/config/tab_component.rb +131 -0
- data/lib/fluxbit/config/text_component.rb +110 -0
- data/lib/fluxbit/config/tooltip_component.rb +11 -0
- data/lib/fluxbit/view_components/codemods/v3_slot_setters.rb +222 -0
- data/lib/fluxbit/view_components/engine.rb +36 -0
- data/lib/fluxbit/view_components/version.rb +7 -0
- data/lib/fluxbit/view_components.rb +30 -0
- data/lib/fluxbit_view_components.rb +3 -0
- data/lib/install/install.rb +64 -0
- data/lib/tasks/fluxbit_view_components_tasks.rake +22 -0
- 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
|