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,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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fluxbit::Form::DatepickerComponent < Fluxbit::Form::Component
4
+ cattr_accessor :styles
5
+
6
+ def call; end
7
+ 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