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,135 @@
|
|
1
|
+
class Fluxbit::CardComponent < Fluxbit::Component
|
2
|
+
# Define default styles for the card and its parts.
|
3
|
+
cattr_accessor :styles, default: {
|
4
|
+
base: "",
|
5
|
+
base_image_left: "flex flex-row",
|
6
|
+
border: "border border-gray-200 dark:border-gray-700",
|
7
|
+
shadow: "shadow-sm",
|
8
|
+
rounded: "rounded-lg",
|
9
|
+
hoverable: "transition-shadow hover:shadow-lg",
|
10
|
+
clickable: {
|
11
|
+
default: "cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700",
|
12
|
+
primary: "cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-800",
|
13
|
+
success: "cursor-pointer hover:bg-green-100 dark:hover:bg-green-800",
|
14
|
+
danger: "cursor-pointer hover:bg-red-100 dark:hover:bg-red-800"
|
15
|
+
},
|
16
|
+
# "flex flex-col items-center bg-white border border-gray-200 rounded-lg shadow-sm md:flex-row md:max-w-xl dark:border-gray-700"
|
17
|
+
header: "px-4 py-2 font-semibold text-gray-900 dark:text-gray-100",
|
18
|
+
body: "px-4 py-2 space-y-4",
|
19
|
+
footer: "px-4 py-2 text-sm text-gray-500 dark:text-gray-400",
|
20
|
+
image_top: "w-full",
|
21
|
+
image_left: "object-cover w-full rounded-t-lg h-96 md:h-auto md:w-48 md:rounded-none md:rounded-s-lg",
|
22
|
+
content_left: "px-4 py-2",
|
23
|
+
colors: {
|
24
|
+
default: "bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100",
|
25
|
+
primary: "bg-blue-50 text-blue-900 border-blue-200 dark:bg-blue-900 dark:text-white dark:border-blue-800",
|
26
|
+
success: "bg-green-50 text-green-900 border-green-200 dark:bg-green-900 dark:text-white dark:border-green-800",
|
27
|
+
danger: "bg-red-50 text-red-900 border-red-200 dark:bg-red-900 dark:text-white dark:border-red-800"
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
renders_one :header
|
32
|
+
renders_one :footer
|
33
|
+
renders_one :section
|
34
|
+
|
35
|
+
# Initializes the card component with various customization options.
|
36
|
+
#
|
37
|
+
# @param color [Symbol] Color theme for the card (e.g., :default, :primary, :success).
|
38
|
+
# @param shadow [Boolean] Whether to apply a drop shadow.
|
39
|
+
# @param border [Boolean] Whether to display a border.
|
40
|
+
# @param rounded [Boolean] Whether the card has rounded corners.
|
41
|
+
# @param hoverable [Boolean] Whether to apply a hover effect.
|
42
|
+
# @param image [String, nil] URL or path of an image to display (optional).
|
43
|
+
# @param image_position [Symbol] Position of the image (:top or :left). Defaults to :top.
|
44
|
+
# @param href [String, nil] Whether the entire card is clickable.
|
45
|
+
# @param tooltip_text [String, nil] Text for a tooltip (optional).
|
46
|
+
# @param tooltip_placement [String] Placement of the tooltip (e.g., "top", "right").
|
47
|
+
# @param tooltip_trigger [String] Trigger event for the tooltip (e.g., "hover", "click").
|
48
|
+
# @param popover_text [String, nil] Text for a popover (optional).
|
49
|
+
# @param popover_placement [String] Placement of the popover.
|
50
|
+
# @param popover_trigger [String] Trigger event for the popover.
|
51
|
+
# @param props [Hash] Additional HTML attributes for the container.
|
52
|
+
def initialize(color: :default, shadow: true, border: true, rounded: true, hoverable: false,
|
53
|
+
image: nil, image_position: :top, image_props: {},
|
54
|
+
tooltip_text: nil, tooltip_placement: "top", tooltip_trigger: "hover",
|
55
|
+
popover_text: nil, popover_placement: "top", popover_trigger: "click",
|
56
|
+
**props)
|
57
|
+
@color = color ? color.to_sym : :default
|
58
|
+
@shadow = shadow
|
59
|
+
@border = border
|
60
|
+
@rounded = rounded
|
61
|
+
@hoverable = hoverable
|
62
|
+
@image = image
|
63
|
+
@image_position = image_position.to_sym
|
64
|
+
@image_props = image_props
|
65
|
+
@tooltip_text = tooltip_text
|
66
|
+
@tooltip_placement = tooltip_placement
|
67
|
+
@tooltip_trigger = tooltip_trigger
|
68
|
+
@popover_text = popover_text
|
69
|
+
@popover_placement = popover_placement
|
70
|
+
@popover_trigger = popover_trigger
|
71
|
+
@props = props
|
72
|
+
@image_props[:src] = @image
|
73
|
+
end
|
74
|
+
|
75
|
+
def before_render
|
76
|
+
add to: @props, first_element: true, class: [
|
77
|
+
styles[:base],
|
78
|
+
@border ? styles[:border] : nil,
|
79
|
+
@shadow ? styles[:shadow] : nil,
|
80
|
+
@rounded ? styles[:rounded] : nil,
|
81
|
+
styles[:colors][@color] || nil,
|
82
|
+
@hoverable ? styles[:hoverable] : nil,
|
83
|
+
@props[:href] ? styles[:clickable][@color] : nil,
|
84
|
+
(@image && @image_position == :left) ? styles[:base_image_left] : nil
|
85
|
+
]
|
86
|
+
@props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
|
87
|
+
end
|
88
|
+
|
89
|
+
def call
|
90
|
+
container_tag = @props[:href] ? :a : :div
|
91
|
+
|
92
|
+
header_html = header ? content_tag(:div, header, class: self.class.styles[:header]) : nil
|
93
|
+
footer_html = footer ? content_tag(:div, footer, class: self.class.styles[:footer]) : nil
|
94
|
+
body_content = section ? section : nil
|
95
|
+
|
96
|
+
if @image && @image_position == :top
|
97
|
+
# Top image layout: image at the top, then header, body, and footer.
|
98
|
+
add(class: styles[:image_top], to: @image_props)
|
99
|
+
image_html = content_tag(:img, nil, **@image_props)
|
100
|
+
body_html = body_content ? content_tag(:div, body_content, class: self.class.styles[:body]) : nil
|
101
|
+
|
102
|
+
content_tag(container_tag, **@props) do
|
103
|
+
concat(image_html)
|
104
|
+
concat(header_html) if header_html
|
105
|
+
concat(body_html) if body_html
|
106
|
+
concat(footer_html) if footer_html
|
107
|
+
end
|
108
|
+
elsif @image && @image_position == :left
|
109
|
+
# Left image layout: image on the left and content on the right in a flex container.
|
110
|
+
add(class: styles[:image_left], to: @image_props)
|
111
|
+
image_html = content_tag(:div, class: "x") do
|
112
|
+
content_tag(:img, nil, **@image_props)
|
113
|
+
end
|
114
|
+
content_inner = "".html_safe
|
115
|
+
content_inner << header_html.to_s if header_html
|
116
|
+
if body_content.present?
|
117
|
+
content_inner << content_tag(:div, body_content, class: self.class.styles[:body] + " " + self.class.styles[:content_left])
|
118
|
+
end
|
119
|
+
content_inner << footer_html.to_s if footer_html
|
120
|
+
|
121
|
+
content_tag(container_tag, **@props) do
|
122
|
+
concat(image_html)
|
123
|
+
concat(content_tag(:div, content_inner, class: "flex-1"))
|
124
|
+
end
|
125
|
+
else
|
126
|
+
# Fallback: render without image or with an unrecognized image_position.
|
127
|
+
body_html = body_content ? content_tag(:div, body_content, class: self.class.styles[:body]) : nil
|
128
|
+
content_tag(container_tag, **@props) do
|
129
|
+
concat(header_html) if header_html
|
130
|
+
concat(body_html) if body_html
|
131
|
+
concat(footer_html) if footer_html
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anyicon"
|
4
|
+
|
5
|
+
class Fluxbit::Component < ViewComponent::Base
|
6
|
+
# Custom class to hold button properties and content
|
7
|
+
ComponentObj = Data.define(:props, :content)
|
8
|
+
|
9
|
+
def initialize(**props)
|
10
|
+
@popover_placement = props.delete(:popover_placement) || :right
|
11
|
+
@popover_trigger = props.delete(:popover_trigger) || :hover # or :click
|
12
|
+
@popover_text = props.delete(:popover_text)
|
13
|
+
|
14
|
+
@tooltip_placement = props.delete(:tooltip_placement) || :right
|
15
|
+
@tooltip_trigger = props.delete(:tooltip_trigger) || :hover # or :click
|
16
|
+
@tooltip_text = props.delete(:tooltip_text)
|
17
|
+
end
|
18
|
+
|
19
|
+
def add(to:, first_element: false, **props)
|
20
|
+
unless props[:class].nil?
|
21
|
+
to[:class] = (to[:class] || "")
|
22
|
+
.split
|
23
|
+
.insert((first_element ? 0 : -1), props[:class])
|
24
|
+
.join(" ")
|
25
|
+
end
|
26
|
+
to
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove_class(elements, from)
|
30
|
+
from.split.reject { |c| c.in?(elements.split) }.join(" ")
|
31
|
+
end
|
32
|
+
|
33
|
+
def options(value, collection: nil, default: nil)
|
34
|
+
if collection.nil?
|
35
|
+
value.nil? ? default : value
|
36
|
+
else
|
37
|
+
value.in?(collection) ? value : default
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def render_popover_or_tooltip
|
42
|
+
safe_join [
|
43
|
+
(@popover_text.nil? ? "" : Fluxbit::PopoverComponent.new(id: target, **(@popover_props || {})).with_content(@popover_text).render_in(view_context)),
|
44
|
+
(@tooltip_text.nil? ? "" : Fluxbit::TooltipComponent.new(id: target, **(@tooltip_props || {})).with_content(@tooltip_text).render_in(view_context))
|
45
|
+
]
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_popover_or_tooltip
|
49
|
+
if popover? || @popover_text.present?
|
50
|
+
@props["data-popover-placement"] = @popover_placement
|
51
|
+
@props["data-popover-trigger"] = @popover_trigger unless @popover_trigger == :hover
|
52
|
+
@props["data-popover-target"] = target
|
53
|
+
end
|
54
|
+
|
55
|
+
if tooltip? || @tooltip_text.present?
|
56
|
+
@props["data-tooltip-placement"] = @tooltip_placement
|
57
|
+
@props["data-tooltip-trigger"] = @tooltip_trigger unless @tooltip_trigger == :hover
|
58
|
+
@props["data-tooltip-target"] = target
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def target
|
63
|
+
@popover_target ||= "#{
|
64
|
+
@props.try('for') ||
|
65
|
+
@props.try(:for) ||
|
66
|
+
(0...10).map { ('a'..'z').to_a[rand(26)] }.join}_target"
|
67
|
+
end
|
68
|
+
|
69
|
+
def anyicon(icon:, **props)
|
70
|
+
Anyicon::Icon.render(icon: icon, **props)
|
71
|
+
end
|
72
|
+
|
73
|
+
def random_id
|
74
|
+
(0...30).map { ("a".."z").to_a[rand(26)] }.join
|
75
|
+
end
|
76
|
+
|
77
|
+
def fx_id
|
78
|
+
@fx_id ||= "#{element_name}-#{random_id}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def element_name
|
82
|
+
self.class.to_s.match(/Fluxbit::(\w+)Component/)[1].underscore
|
83
|
+
rescue
|
84
|
+
"any"
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The `Fluxbit::FlexComponent` is a component for rendering customizable flex containers.
|
4
|
+
# It extends `Fluxbit::Component` and provides options for configuring the flex container's
|
5
|
+
# appearance and behavior. You can control the flex direction, alignment, wrapping, gap,
|
6
|
+
# and other attributes. This component is useful for creating responsive layouts and
|
7
|
+
# aligning content dynamically.
|
8
|
+
class Fluxbit::FlexComponent < Fluxbit::Component
|
9
|
+
include Fluxbit::Config::FlexComponent
|
10
|
+
|
11
|
+
# Initializes the flex component with various customization options.
|
12
|
+
#
|
13
|
+
# @param vertical [Boolean] Whether the flex direction is vertical. Defaults to `false`.
|
14
|
+
# @param reverse [Boolean] Whether the flex direction is reversed. Defaults to `false`.
|
15
|
+
# @param justify_content [Symbol] The justification of content. Options include `:start`, `:end`, `:center`, `:space_around`, `:space_between`, `:space_evenly`, etc. Defaults to `:center`.
|
16
|
+
# @param align_items [Symbol] The alignment of items. Options include `:start`, `:end`, `:center`, `:baseline`, `:stretch`. Defaults to `:center`.
|
17
|
+
# @param wrap [Boolean] Whether the flex container should wrap. Defaults to `false`.
|
18
|
+
# @param wrap_reverse [Boolean] Whether the flex container should wrap in reverse. Defaults to `false`.
|
19
|
+
# @param gap [Integer] The gap between flex items. Defaults to `0`.
|
20
|
+
# @param props [Hash] Additional HTML attributes for the container.
|
21
|
+
def initialize(**props)
|
22
|
+
@props = props
|
23
|
+
|
24
|
+
declare_classes(@props)
|
25
|
+
%i[vertical reverse justify_content align_items wrap wrap_reverse gap].each do |key|
|
26
|
+
@props.delete(key)
|
27
|
+
end
|
28
|
+
|
29
|
+
styles[:resolutions].each do |resolution|
|
30
|
+
if @props.key?(resolution)
|
31
|
+
declare_classes(@props.delete(resolution), resolution)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def call
|
37
|
+
content_tag :div, content, @props
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def declare_resolution(resolution)
|
43
|
+
return "" if resolution.nil?
|
44
|
+
"#{resolution}:"
|
45
|
+
end
|
46
|
+
|
47
|
+
def declare_classes(props = {}, resolution = nil)
|
48
|
+
if props.key?(:vertical)
|
49
|
+
vertical = props[:vertical]
|
50
|
+
reverse = props[:reverse] || false
|
51
|
+
add class: "#{declare_resolution(resolution)}#{direction(vertical, reverse)}", to: @props, first_element: true
|
52
|
+
end
|
53
|
+
|
54
|
+
if props.key?(:wrap)
|
55
|
+
wrap = props[:wrap]
|
56
|
+
wrap_reverse = props[:wrap_reverse] || false
|
57
|
+
add class: "#{declare_resolution(resolution)}#{wrap(wrap, wrap_reverse)}", to: @props, first_element: true
|
58
|
+
end
|
59
|
+
|
60
|
+
if props.key?(:justify_content)
|
61
|
+
justify_content = props[:justify_content]
|
62
|
+
add class: "#{declare_resolution(resolution)}#{styles[:justify_content][justify_content.to_sym]}", to: @props, first_element: true
|
63
|
+
end
|
64
|
+
|
65
|
+
if props.key?(:align_items)
|
66
|
+
align_items = props[:align_items]
|
67
|
+
add class: "#{declare_resolution(resolution)}#{styles[:align_items][align_items.to_sym]}", to: @props, first_element: true
|
68
|
+
end
|
69
|
+
|
70
|
+
if props.key?(:gap)
|
71
|
+
gap = props[:gap]
|
72
|
+
add class: "#{declare_resolution(resolution)}#{styles[:gap][gap.to_i]}", to: @props, first_element: true
|
73
|
+
end
|
74
|
+
|
75
|
+
add(class: styles[:base], to: @props, first_element: true) if resolution.nil?
|
76
|
+
end
|
77
|
+
|
78
|
+
def wrap(wrap, reverse)
|
79
|
+
if wrap
|
80
|
+
reverse ? styles[:wrap][:wrap_reverse] : styles[:wrap][:wrap]
|
81
|
+
else
|
82
|
+
styles[:wrap][:nowrap]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def direction(vertical, reverse)
|
87
|
+
if vertical
|
88
|
+
reverse ? styles[:direction][:vertical_reverse] : styles[:direction][:vertical]
|
89
|
+
else
|
90
|
+
reverse ? styles[:direction][:horizontal_reverse] : styles[:direction][:horizontal]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Fluxbit::Form::CheckboxInputComponent < Fluxbit::Form::Component
|
4
|
+
# rubocop: disable Layout/LineLength
|
5
|
+
cattr_accessor :styles do
|
6
|
+
{
|
7
|
+
checkbox: "rounded-sm",
|
8
|
+
base: "w-4 h-4 text-blue-600 bg-slate-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-slate-700 dark:border-slate-600",
|
9
|
+
label: {
|
10
|
+
with_helper: "font-medium text-slate-900 dark:text-slate-300",
|
11
|
+
base: "ml-2 text-sm font-medium text-slate-900 dark:text-slate-300"
|
12
|
+
},
|
13
|
+
input_div: "flex items-center h-5",
|
14
|
+
helper_div: "ml-2 text-sm",
|
15
|
+
no_helper_div: "flex items-center"
|
16
|
+
}
|
17
|
+
end
|
18
|
+
# rubocop: enable Layout/LineLength
|
19
|
+
|
20
|
+
def initialize(form: nil, field: nil, label: nil, helper_text: nil, helper_popover: nil,
|
21
|
+
helper_popover_placement: "right", **props)
|
22
|
+
super
|
23
|
+
@form = form
|
24
|
+
@field = field
|
25
|
+
@object = form&.object
|
26
|
+
@props = props
|
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
|
+
|
32
|
+
@props[:type] = @props[:type].to_s.in?(%w[checkbox radio]) ? @props[:type].to_s : "checkbox"
|
33
|
+
add(class: styles[:checkbox], to: @props, first_element: true) if @props[:type] == "checkbox"
|
34
|
+
add(class: styles[:base], to: @props, first_element: true)
|
35
|
+
end
|
36
|
+
|
37
|
+
def input
|
38
|
+
if @form.nil?
|
39
|
+
content_tag :input, content, @props
|
40
|
+
else
|
41
|
+
@form.text_field(@field, **@props)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def call
|
46
|
+
if @helper_text
|
47
|
+
content_tag :div, { class: "flex" } do
|
48
|
+
concat content_tag(:div, input, { class: styles[:input_div] })
|
49
|
+
concat content_tag(:div, { class: styles[:helper_div] }) do
|
50
|
+
concat label
|
51
|
+
concat helper_text
|
52
|
+
end
|
53
|
+
end
|
54
|
+
else
|
55
|
+
content_tag :div, { class: styles[:no_helper_div] } do
|
56
|
+
concat input
|
57
|
+
concat label
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Interface to Inputs
|
4
|
+
class Fluxbit::Form::Component < Fluxbit::Component
|
5
|
+
def id
|
6
|
+
return @id ||= random_id if @props[:id].nil? && @form.nil?
|
7
|
+
return @props[:id] unless @props[:id].nil?
|
8
|
+
|
9
|
+
"#{@form.object_name}_#{@field}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def define_helper_text(helper_text, object, field)
|
13
|
+
return nil if helper_text.is_a? FalseClass
|
14
|
+
|
15
|
+
if helper_text.nil? && !object.nil? && !field.nil?
|
16
|
+
helper_text = I18n.t(
|
17
|
+
field,
|
18
|
+
scope: [ :activerecord, :helper_text, object.class.name.underscore.to_sym ],
|
19
|
+
default: nil
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
(helper_text.is_a?(Array) ? helper_text : [ helper_text ]) + errors
|
24
|
+
end
|
25
|
+
|
26
|
+
def define_helper_popover(helper_popover, object, field)
|
27
|
+
return helper_popover if helper_popover != false && !helper_popover.nil?
|
28
|
+
|
29
|
+
I18n.t(field, scope: [ :activerecord, :helper_popover, object.class.name.underscore.to_sym ], default: nil)
|
30
|
+
end
|
31
|
+
|
32
|
+
def label_value(label, object, field, id)
|
33
|
+
return object.class.human_attribute_name(field) if label.nil? && !object.nil? && !field.nil?
|
34
|
+
return id.to_s.humanize if label.nil? && !id.nil?
|
35
|
+
return label unless label.nil?
|
36
|
+
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def label
|
41
|
+
return "" if @label.blank?
|
42
|
+
|
43
|
+
Fluxbit::Form::LabelComponent.new(
|
44
|
+
for: id,
|
45
|
+
color: @color,
|
46
|
+
helper_popover: @helper_popover,
|
47
|
+
helper_popover_placement: @helper_popover_placement,
|
48
|
+
class: @label_class
|
49
|
+
).with_content(@label).render_in(view_context)
|
50
|
+
end
|
51
|
+
|
52
|
+
def errors
|
53
|
+
return [] unless @object&.errors&.any?
|
54
|
+
|
55
|
+
@object.errors.filter { |f| f.attribute == @field }.map(&:full_message)
|
56
|
+
end
|
57
|
+
|
58
|
+
def helper_text
|
59
|
+
return "" if @helper_text.blank?
|
60
|
+
|
61
|
+
# safe_join(
|
62
|
+
# @helper_text.compact.map do |text|
|
63
|
+
# Fluxbit::HelperTextComponent.new(color: @color).with_content(text).render_in(view_context)
|
64
|
+
# end
|
65
|
+
# )
|
66
|
+
|
67
|
+
@helper_text.compact.map do |text|
|
68
|
+
concat Fluxbit::Form::HelperTextComponent.new(color: @color).with_content(text).render_in(view_context)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Fluxbit::Form::FormBuilderComponent < Fluxbit::Component
|
4
|
+
TEXT_TYPES = %w[text email password number search time date datetime-local].freeze
|
5
|
+
INPUT_TYPES = %w[upload upload_image toggle textarea spacer select select_free range radio checkbox].freeze
|
6
|
+
|
7
|
+
cattr_accessor :styles
|
8
|
+
self.styles = %i[
|
9
|
+
grid-cols-1
|
10
|
+
grid-cols-2
|
11
|
+
grid-cols-3
|
12
|
+
grid-cols-4
|
13
|
+
gap-1
|
14
|
+
gap-2
|
15
|
+
gap-3
|
16
|
+
gap-4
|
17
|
+
col-span-1
|
18
|
+
col-span-2
|
19
|
+
col-span-3
|
20
|
+
col-span-4
|
21
|
+
]
|
22
|
+
|
23
|
+
renders_many :elements,
|
24
|
+
lambda { |**props, &block|
|
25
|
+
choose_element(props.merge({ form: @form }), block)
|
26
|
+
}
|
27
|
+
|
28
|
+
def initialize(form: nil, gap: 4, grid_cols: 2, show_errors: true, elements: [], **props)
|
29
|
+
super
|
30
|
+
@form = form
|
31
|
+
@object = form&.object
|
32
|
+
@elements = elements
|
33
|
+
@props = props
|
34
|
+
@gap = gap
|
35
|
+
@grid_cols = grid_cols
|
36
|
+
@show_errors = show_errors
|
37
|
+
add(class: grid_styles, to: @props) unless grid_styles && grid_styles.empty?
|
38
|
+
end
|
39
|
+
|
40
|
+
def grid_styles
|
41
|
+
return "grid gap-#{@gap} grid-cols-#{@grid_cols}" if @grid_cols.class != Hash
|
42
|
+
|
43
|
+
"grid gap-#{@gap} " + @grid_cols.map do |size, col|
|
44
|
+
"#{size == :default ? '' : "#{size}:"}grid-cols-#{col}"
|
45
|
+
end.join(" ")
|
46
|
+
end
|
47
|
+
|
48
|
+
def colspan(colspan_element)
|
49
|
+
return "" if colspan_element.nil?
|
50
|
+
return "col-span-#{colspan_element}" if colspan_element.class != Hash
|
51
|
+
|
52
|
+
colspan_element.map { |size, col| "#{size == :default ? '' : "#{size}:"}col-span-#{col}" }.join(" ")
|
53
|
+
end
|
54
|
+
|
55
|
+
def errors?
|
56
|
+
return "" if !@show_errors || @object.nil? || @object.errors&.none?
|
57
|
+
|
58
|
+
content_tag :div, class: "col-span-4" do
|
59
|
+
Fluxbit::AlertComponent.new(type: :danger).with_content(
|
60
|
+
I18n.t(
|
61
|
+
"form_error",
|
62
|
+
scope: [ :activerecord, :messages, @object.class.name.underscore.to_sym ],
|
63
|
+
count: @object.errors.count,
|
64
|
+
default: "#{pluralize(@object.errors.count, 'error')}."
|
65
|
+
)
|
66
|
+
).render_in(view_context)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def choose_element(kwargs, block = nil)
|
71
|
+
colspan_element = kwargs.key?(:colspan) ? kwargs.delete(:colspan) : nil
|
72
|
+
outer_div = kwargs.key?(:outer_div) ? kwargs.delete(:outer_div) : ""
|
73
|
+
outer_div += colspan(colspan_element)
|
74
|
+
return content_tag(:div, block.call, class: outer_div) if kwargs[:type] == :html
|
75
|
+
|
76
|
+
kwargs[:show_errors] = false if kwargs[:type] == :group
|
77
|
+
component_klass = "Fluxbit::#{if (TEXT_TYPES + INPUT_TYPES + [ 'label', 'group', '' ]).include?(kwargs[:type].to_s)
|
78
|
+
'Form::'
|
79
|
+
else
|
80
|
+
''
|
81
|
+
end}#{element_type(kwargs[:type])}Component".constantize
|
82
|
+
unless kwargs[:with_content]
|
83
|
+
return content_tag(:div, render(component_klass.new(**kwargs), &block), class: outer_div)
|
84
|
+
end
|
85
|
+
|
86
|
+
content = kwargs.delete(:with_content)
|
87
|
+
content_tag :div, render(component_klass.new(**kwargs).with_content(content), &block), class: outer_div
|
88
|
+
end
|
89
|
+
|
90
|
+
def element_type(type)
|
91
|
+
return "TextInput" if type.nil? || type.to_s.in?(TEXT_TYPES)
|
92
|
+
return type.to_s.concat("_input").camelcase if type.to_s.in?(INPUT_TYPES)
|
93
|
+
|
94
|
+
case type
|
95
|
+
when :submit
|
96
|
+
"Button"
|
97
|
+
when :group
|
98
|
+
"FormBuilder"
|
99
|
+
else
|
100
|
+
type.to_s.camelcase
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def generate_elements
|
105
|
+
return elements if elements?
|
106
|
+
|
107
|
+
safe_join(*@elements.map { |element| choose_element(element.merge({ form: @form }), nil) })
|
108
|
+
end
|
109
|
+
|
110
|
+
def generate_div
|
111
|
+
safe_join errors?, content_tag(:div, generate_elements, @props)
|
112
|
+
end
|
113
|
+
|
114
|
+
def call
|
115
|
+
generate_div
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The `Fluxbit::HelperTextComponent` is a component for rendering customizable helper text elements.
|
4
|
+
# It extends `Fluxbit::Component` and provides options for configuring the helper text's
|
5
|
+
# appearance and behavior. You can control the helper text's color and other attributes.
|
6
|
+
# The helper text can have various styles applied based on the provided properties.
|
7
|
+
class Fluxbit::Form::HelperTextComponent < Fluxbit::Form::Component
|
8
|
+
include Fluxbit::Config::Form::HelperTextComponent
|
9
|
+
|
10
|
+
# Initializes the helper text component with the given properties.
|
11
|
+
#
|
12
|
+
# @param [Symbol] color (:default) The color of the helper text.
|
13
|
+
# @param [Hash] props The properties to customize the helper text.
|
14
|
+
# @option props [Hash] **props Remaining options declared as HTML attributes, applied to the helper text element.
|
15
|
+
def initialize(color: nil, **props)
|
16
|
+
super
|
17
|
+
@props = props
|
18
|
+
color = @@color unless color.in? %i[info default success failure warning]
|
19
|
+
add class: style(color), to: @props, first_element: true
|
20
|
+
end
|
21
|
+
|
22
|
+
def style(color)
|
23
|
+
styles[:base] + styles[:colors][color]
|
24
|
+
end
|
25
|
+
|
26
|
+
def call
|
27
|
+
content_tag :p, content, @props
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Fluxbit::Form::LabelComponent < Fluxbit::Form::Component
|
4
|
+
cattr_accessor :styles do
|
5
|
+
{
|
6
|
+
base: "flex font-medium",
|
7
|
+
colors: {
|
8
|
+
default: "text-gray-900 dark:text-white",
|
9
|
+
success: "text-green-700 dark:text-green-500",
|
10
|
+
failure: "text-red-700 dark:text-red-500",
|
11
|
+
info: "text-cyan-500 dark:text-cyan-600",
|
12
|
+
warning: "text-yellow-500 dark:text-yellow-600"
|
13
|
+
},
|
14
|
+
sizes: {
|
15
|
+
sm: "text-sm",
|
16
|
+
md: "text-md",
|
17
|
+
lg: "text-lg"
|
18
|
+
},
|
19
|
+
helper_popover: "px-2 text-slate-400"
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(color: :default, form: nil, with_content: nil, helper_text: nil,
|
24
|
+
sizing: :sm, helper_popover: nil, helper_popover_placement: "right", **props)
|
25
|
+
super
|
26
|
+
@props = props
|
27
|
+
@sizing = sizing.in?(styles[:sizes].keys) ? sizing : :sm
|
28
|
+
@with_content = with_content
|
29
|
+
@helper_text = helper_text.is_a?(Array) ? helper_text : [ helper_text ]
|
30
|
+
@helper_popover = helper_popover
|
31
|
+
@helper_popover_placement = helper_popover_placement
|
32
|
+
color = :default unless color.in? %i[info default success failure warning]
|
33
|
+
add class: styles[:colors][color], to: @props, first_element: true
|
34
|
+
add class: styles[:base], to: @props, first_element: true
|
35
|
+
add class: styles[:sizes][@sizing], to: @props, first_element: true
|
36
|
+
end
|
37
|
+
|
38
|
+
def span_helper_popover
|
39
|
+
return "" if @helper_popover.nil?
|
40
|
+
|
41
|
+
content_tag :span,
|
42
|
+
anyicon(icon: "heroicons_solid:question-mark-circle", class: "w-4 h-4"),
|
43
|
+
{
|
44
|
+
"data-popover-placement": @helper_popover_placement,
|
45
|
+
"data-popover-target": target,
|
46
|
+
class: styles[:helper_popover]
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def render_popover
|
51
|
+
return "" if @helper_popover.nil?
|
52
|
+
|
53
|
+
Fluxbit::PopoverComponent.new(id: target).with_content(@helper_popover).render_in(view_context)
|
54
|
+
end
|
55
|
+
|
56
|
+
def call
|
57
|
+
safe_join(
|
58
|
+
[
|
59
|
+
content_tag(:label, safe_join([ content || @with_content, span_helper_popover ]), @props),
|
60
|
+
helper_text,
|
61
|
+
render_popover
|
62
|
+
]
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Fluxbit::Form::RadioInputComponent < Fluxbit::Form::Component
|
4
|
+
def initialize(**kwargs, &block)
|
5
|
+
super
|
6
|
+
kwargs[:type] = :radio
|
7
|
+
|
8
|
+
@component_klass = "Fluxbit::Form::CheckboxInputComponent".constantize
|
9
|
+
@kwargs = kwargs
|
10
|
+
@block = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def call
|
14
|
+
if @kwargs[:with_content]
|
15
|
+
content = @kwargs.delete(:with_content)
|
16
|
+
render(@component_klass.new(**@kwargs).with_content(content), &@block)
|
17
|
+
else
|
18
|
+
render(@component_klass.new(**@kwargs), &@block)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|