fluxbit_view_components 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +10 -0
- data/app/assets/javascripts/fluxbit_view_components/assigner_controller.js +49 -0
- data/app/assets/javascripts/fluxbit_view_components/auto_submit_controller.js +39 -0
- data/app/assets/javascripts/fluxbit_view_components/drawer_controller.js +135 -0
- data/app/assets/javascripts/fluxbit_view_components/index.js +56 -0
- data/app/assets/javascripts/fluxbit_view_components/method_link_controller.js +143 -0
- data/app/assets/javascripts/fluxbit_view_components/modal_controller.js +118 -0
- data/app/assets/javascripts/fluxbit_view_components/password_controller.js +170 -0
- data/app/assets/javascripts/fluxbit_view_components/progress_controller.js +374 -0
- data/app/assets/javascripts/fluxbit_view_components/row_click_controller.js +32 -0
- data/app/assets/javascripts/fluxbit_view_components/select_all_controller.js +122 -0
- data/app/assets/javascripts/fluxbit_view_components/spinner_percent_controller.js +174 -0
- data/app/assets/javascripts/fluxbit_view_components/theme_button_controller.js +90 -0
- data/app/assets/javascripts/fluxbit_view_components.js +1175 -0
- data/app/components/fluxbit/accordion_component.rb +125 -0
- data/app/components/fluxbit/alert_component.rb +8 -8
- data/app/components/fluxbit/avatar_component.rb +11 -12
- data/app/components/fluxbit/avatar_group_component.rb +1 -1
- data/app/components/fluxbit/badge_component.rb +8 -7
- data/app/components/fluxbit/banner_component.rb +139 -0
- data/app/components/fluxbit/bottom_navigation_component.rb +437 -0
- data/app/components/fluxbit/breadcrumb_component.rb +66 -0
- data/app/components/fluxbit/button_component.rb +39 -11
- data/app/components/fluxbit/button_group_component.rb +1 -1
- data/app/components/fluxbit/card_component.rb +26 -23
- data/app/components/fluxbit/carousel_component.rb +154 -0
- data/app/components/fluxbit/component.rb +24 -3
- data/app/components/fluxbit/drawer_component.html.erb +30 -0
- data/app/components/fluxbit/drawer_component.rb +125 -0
- data/app/components/fluxbit/dropdown_component.rb +41 -0
- data/app/components/fluxbit/dropdown_item_component.rb +68 -0
- data/app/components/fluxbit/flex_component.rb +1 -1
- data/app/components/fluxbit/form/check_box_component.rb +56 -0
- data/app/components/fluxbit/form/component.rb +27 -26
- data/app/components/fluxbit/form/dropzone_component.html.erb +39 -0
- data/app/components/fluxbit/form/dropzone_component.rb +39 -0
- data/app/components/fluxbit/form/field_component.rb +28 -0
- data/app/components/fluxbit/form/form_builder_component.rb +1 -1
- data/app/components/fluxbit/form/{helper_text_component.rb → help_text_component.rb} +9 -4
- data/app/components/fluxbit/form/label_component.rb +40 -30
- data/app/components/fluxbit/form/password_component.rb +247 -0
- data/app/components/fluxbit/form/radio_group_button_component.rb +126 -0
- data/app/components/fluxbit/form/range_component.rb +52 -0
- data/app/components/fluxbit/form/select_component.rb +185 -0
- data/app/components/fluxbit/form/text_field_component.rb +185 -0
- data/app/components/fluxbit/form/toggle_component.html.erb +23 -0
- data/app/components/fluxbit/form/toggle_component.rb +81 -0
- data/app/components/fluxbit/form/upload_image_component.html.erb +50 -0
- data/app/components/fluxbit/form/upload_image_component.rb +61 -0
- data/app/components/fluxbit/gravatar_component.rb +7 -0
- data/app/components/fluxbit/icon_helpers.rb +167 -0
- data/app/components/fluxbit/link_component.rb +42 -0
- data/app/components/fluxbit/modal_component.rb +28 -31
- data/app/components/fluxbit/pagination_component.rb +206 -0
- data/app/components/fluxbit/popover_component.rb +14 -14
- data/app/components/fluxbit/progress_component.rb +196 -0
- data/app/components/fluxbit/skeleton_component.rb +237 -0
- data/app/components/fluxbit/speed_dial_action_component.html.erb +30 -0
- data/app/components/fluxbit/speed_dial_action_component.rb +59 -0
- data/app/components/fluxbit/speed_dial_component.html.erb +33 -0
- data/app/components/fluxbit/speed_dial_component.rb +73 -0
- data/app/components/fluxbit/spinner_component.rb +71 -0
- data/app/components/fluxbit/spinner_percent_component.rb +174 -0
- data/app/components/fluxbit/stepper_component.rb +223 -0
- data/app/components/fluxbit/tab_component.rb +44 -25
- data/app/components/fluxbit/table_component.rb +186 -0
- data/app/components/fluxbit/table_group_component.rb +28 -0
- data/app/components/fluxbit/theme_button_component.rb +64 -0
- data/app/components/fluxbit/timeline_component.rb +63 -0
- data/app/components/fluxbit/timeline_item_component.html.erb +64 -0
- data/app/components/fluxbit/timeline_item_component.rb +78 -0
- data/app/components/fluxbit/tooltip_component.rb +2 -2
- data/app/helpers/fluxbit/components_helper.rb +93 -51
- data/app/helpers/fluxbit/form_builder.rb +136 -0
- data/app/helpers/fluxbit/view_helper.rb +71 -0
- data/config/locales/en.yml +37 -4
- data/config/locales/pt-BR.yml +36 -0
- data/lib/fluxbit/config/accordion_component.rb +73 -0
- data/lib/fluxbit/config/avatar_component.rb +11 -11
- data/lib/fluxbit/config/badge_component.rb +14 -11
- data/lib/fluxbit/config/banner_component.rb +60 -0
- data/lib/fluxbit/config/bottom_navigation_component.rb +74 -0
- data/lib/fluxbit/config/breadcrumb_component.rb +24 -0
- data/lib/fluxbit/config/button_component.rb +6 -4
- data/lib/fluxbit/config/card_component.rb +23 -12
- data/lib/fluxbit/config/carousel_component.rb +33 -0
- data/lib/fluxbit/config/drawer_component.rb +48 -0
- data/lib/fluxbit/config/dropdown_component.rb +29 -0
- data/lib/fluxbit/config/form/check_box_component.rb +19 -0
- data/lib/fluxbit/config/form/dropzone_component.rb +20 -0
- data/lib/fluxbit/config/form/{helper_text_component.rb → help_text_component.rb} +2 -2
- data/lib/fluxbit/config/form/label_component.rb +31 -0
- data/lib/fluxbit/config/form/password_component.rb +19 -0
- data/lib/fluxbit/config/form/radio_group_button_component.rb +24 -0
- data/lib/fluxbit/config/form/range_component.rb +15 -0
- data/lib/fluxbit/config/form/text_field_component.rb +76 -0
- data/lib/fluxbit/config/form/toggle_component.rb +79 -0
- data/lib/fluxbit/config/link_component.rb +24 -0
- data/lib/fluxbit/config/modal_component.rb +1 -1
- data/lib/fluxbit/config/pagination_component.rb +31 -0
- data/lib/fluxbit/config/popover_component.rb +1 -1
- data/lib/fluxbit/config/progress_component.rb +63 -0
- data/lib/fluxbit/config/skeleton_component.rb +82 -0
- data/lib/fluxbit/config/speed_dial_component.rb +50 -0
- data/lib/fluxbit/config/spinner_component.rb +30 -0
- data/lib/fluxbit/config/spinner_percent_component.rb +61 -0
- data/lib/fluxbit/config/stepper_component.rb +299 -0
- data/lib/fluxbit/config/tab_component.rb +6 -0
- data/lib/fluxbit/config/table_component.rb +75 -0
- data/lib/fluxbit/config/theme_button_component.rb +19 -0
- data/lib/fluxbit/config/timeline_component.rb +77 -0
- data/lib/fluxbit/view_components/engine.rb +11 -3
- data/lib/fluxbit/view_components/version.rb +1 -1
- data/lib/fluxbit/view_components.rb +27 -1
- data/lib/generators/fluxbit/devise_views_generator.rb +116 -0
- data/lib/generators/fluxbit/pagy_generator.rb +39 -0
- data/lib/generators/fluxbit/scaffold_generator.rb +165 -0
- data/lib/generators/fluxbit/templates/_alert.html.erb.tt +1 -0
- data/lib/generators/fluxbit/templates/_flash.html.erb.tt +15 -0
- data/lib/generators/fluxbit/templates/_form.html.erb.tt +38 -0
- data/lib/generators/fluxbit/templates/_metadata.html.erb.tt +44 -0
- data/lib/generators/fluxbit/templates/controller.rb.tt +406 -0
- data/lib/generators/fluxbit/templates/create.turbo_stream.erb.tt +7 -0
- data/lib/generators/fluxbit/templates/destroy.turbo_stream.erb.tt +3 -0
- data/lib/generators/fluxbit/templates/destroy_all.turbo_stream.erb.tt +9 -0
- data/lib/generators/fluxbit/templates/devise_views/confirmations/new.html.erb +11 -0
- data/lib/generators/fluxbit/templates/devise_views/layouts/devise.html.erb +64 -0
- data/lib/generators/fluxbit/templates/devise_views/mailer/confirmation_instructions.html.erb +5 -0
- data/lib/generators/fluxbit/templates/devise_views/mailer/email_changed.html.erb +7 -0
- data/lib/generators/fluxbit/templates/devise_views/mailer/password_changed.html.erb +3 -0
- data/lib/generators/fluxbit/templates/devise_views/mailer/reset_password_instructions.html.erb +8 -0
- data/lib/generators/fluxbit/templates/devise_views/mailer/unlock_instructions.html.erb +7 -0
- data/lib/generators/fluxbit/templates/devise_views/passwords/edit.html.erb +29 -0
- data/lib/generators/fluxbit/templates/devise_views/passwords/new.html.erb +11 -0
- data/lib/generators/fluxbit/templates/devise_views/registrations/edit.html.erb +43 -0
- data/lib/generators/fluxbit/templates/devise_views/registrations/new.html.erb +34 -0
- data/lib/generators/fluxbit/templates/devise_views/sessions/new.html.erb +15 -0
- data/lib/generators/fluxbit/templates/devise_views/shared/_error_messages.html.erb +14 -0
- data/lib/generators/fluxbit/templates/devise_views/shared/_links.html.erb +25 -0
- data/lib/generators/fluxbit/templates/devise_views/unlocks/new.html.erb +11 -0
- data/lib/generators/fluxbit/templates/edit.html.erb.tt +47 -0
- data/lib/generators/fluxbit/templates/fluxbit_pagy.css +27 -0
- data/lib/generators/fluxbit/templates/i18n.en.yml.tt +121 -0
- data/lib/generators/fluxbit/templates/i18n.pt-BR.yml.tt +121 -0
- data/lib/generators/fluxbit/templates/index.html.erb.tt +254 -0
- data/lib/generators/fluxbit/templates/index.json.jbuilder.tt +33 -0
- data/lib/generators/fluxbit/templates/new.html.erb.tt +47 -0
- data/lib/generators/fluxbit/templates/partial.html.erb.tt +61 -0
- data/lib/generators/fluxbit/templates/policy.rb.tt +36 -0
- data/lib/generators/fluxbit/templates/send_alert_via_drawer.erb.tt +10 -0
- data/lib/generators/fluxbit/templates/show.html.erb.tt +44 -0
- data/lib/generators/fluxbit/templates/show.json.jbuilder.tt +6 -0
- data/lib/generators/fluxbit/templates/update.turbo_stream.erb.tt +10 -0
- data/lib/generators/fluxbit/templates/update_all.turbo_stream.erb.tt +20 -0
- data/lib/install/install.rb +61 -3
- metadata +127 -35
- data/LICENSE.txt +0 -20
- data/app/components/fluxbit/form/checkbox_input_component.rb +0 -61
- data/app/components/fluxbit/form/datepicker_component.rb +0 -7
- data/app/components/fluxbit/form/radio_input_component.rb +0 -21
- data/app/components/fluxbit/form/range_input_component.rb +0 -51
- data/app/components/fluxbit/form/select_free_input_component.rb +0 -77
- data/app/components/fluxbit/form/select_input_component.rb +0 -21
- data/app/components/fluxbit/form/spacer_input_component.rb +0 -12
- data/app/components/fluxbit/form/text_input_component.rb +0 -225
- data/app/components/fluxbit/form/textarea_input_component.rb +0 -57
- data/app/components/fluxbit/form/toggle_input_component.rb +0 -166
- data/app/components/fluxbit/form/upload_image_input_component.html.erb +0 -48
- data/app/components/fluxbit/form/upload_image_input_component.rb +0 -61
- data/app/components/fluxbit/form/upload_input_component.html.erb +0 -12
- data/app/components/fluxbit/form/upload_input_component.rb +0 -47
- data/app/helpers/fluxbit/classes_helper.rb +0 -9
|
@@ -16,7 +16,7 @@ class Fluxbit::PopoverComponent < Fluxbit::Component
|
|
|
16
16
|
# @option props [Boolean] :has_arrow (true) Determines if an arrow should be displayed on the popover.
|
|
17
17
|
# @option props [String] :image (nil) The URL of an image to be displayed in the popover.
|
|
18
18
|
# @option props [Symbol] :image_position (:right) The position of the image relative to the content (:left or :right).
|
|
19
|
-
# @option props [Hash] :
|
|
19
|
+
# @option props [Hash] :image_html ({}) Additional HTML attributes for the image element.
|
|
20
20
|
# @option props [Symbol, String] :size (2) The size of the popover (0 to 4).
|
|
21
21
|
# @option props [String] :remove_class ('') Classes to be removed from the default popover class list.
|
|
22
22
|
# @option props [Hash] **props Remaining options declared as HTML attributes, applied to the popover container.
|
|
@@ -27,31 +27,31 @@ class Fluxbit::PopoverComponent < Fluxbit::Component
|
|
|
27
27
|
@has_arrow = options @props.delete(:has_arrow), default: @@has_arrow
|
|
28
28
|
@image = @props.delete(:image)
|
|
29
29
|
@image_position = options @props.delete(:image_position), default: @@image_position
|
|
30
|
-
@
|
|
30
|
+
@image_html = options @props.delete(:image_html), default: @@image_html
|
|
31
31
|
@props["data-popover"] = "data-popover"
|
|
32
32
|
@props["role"] = "tooltip"
|
|
33
33
|
|
|
34
34
|
add(class: [ styles[:base], styles[:size][@props.delete(:size) || @@size] ], to: @props)
|
|
35
|
-
add(class: styles[:image_content][:image], to: @
|
|
36
|
-
@
|
|
35
|
+
add(class: styles[:image_content][:image], to: @image_html)
|
|
36
|
+
@image_html[:src] = @image
|
|
37
37
|
|
|
38
38
|
@props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
|
|
39
|
-
@
|
|
39
|
+
@image_html[:class] = remove_class(@props.delete(:remove_class) || "", @image_html[:class])
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def call
|
|
43
|
-
|
|
43
|
+
tag.div(**@props) do
|
|
44
44
|
concat div_title unless @title.blank?
|
|
45
|
-
concat (
|
|
45
|
+
concat (tag.div(class: styles[@image.blank? ? :content : :image_base]) do
|
|
46
46
|
if @image.blank?
|
|
47
47
|
content
|
|
48
48
|
else
|
|
49
49
|
if @image_position == :left
|
|
50
|
-
concat
|
|
51
|
-
concat
|
|
50
|
+
concat tag.img(**@image_html)
|
|
51
|
+
concat tag.div(content, class: styles[:image_content][:text])
|
|
52
52
|
else
|
|
53
|
-
concat
|
|
54
|
-
concat
|
|
53
|
+
concat tag.div(content, class: styles[:image_content][:text])
|
|
54
|
+
concat tag.img(**@image_html)
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
57
|
end)
|
|
@@ -60,12 +60,12 @@ class Fluxbit::PopoverComponent < Fluxbit::Component
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def popper_arrow
|
|
63
|
-
|
|
63
|
+
tag.div("data-popper-arrow" => true)
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def div_title
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
tag.div(class: styles[:title][:div]) do
|
|
68
|
+
tag.h3(@title, class: styles[:title][:h3])
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# The `Fluxbit::ProgressComponent` is a customizable progress bar component that extends `Fluxbit::Component`.
|
|
5
|
+
# It allows you to create progress indicators with various styles, sizes, colors, and label positioning options
|
|
6
|
+
# to display completion status or loading progress.
|
|
7
|
+
#
|
|
8
|
+
# @example Basic usage
|
|
9
|
+
# = fx_progress(progress: 45)
|
|
10
|
+
#
|
|
11
|
+
# @example With labels
|
|
12
|
+
# = fx_progress(progress: 75, text_label: "Loading", label_progress: true)
|
|
13
|
+
#
|
|
14
|
+
# @see docs/02_Components/Progress.md For detailed documentation.
|
|
15
|
+
class Fluxbit::ProgressComponent < Fluxbit::Component
|
|
16
|
+
include Fluxbit::Config::ProgressComponent
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# Initializes the progress component with the given properties.
|
|
20
|
+
#
|
|
21
|
+
# @param [Hash] **props The properties to customize the component.
|
|
22
|
+
# @option props [Integer] :progress (0) The progress percentage (0-100).
|
|
23
|
+
# @option props [Symbol, String] :color (:default) The color theme of the progress bar.
|
|
24
|
+
# @option props [Integer] :size (1) The size of the progress bar (0-3).
|
|
25
|
+
# @option props [String] :text_label (nil) Label text to display with the progress bar.
|
|
26
|
+
# @option props [Boolean] :label_progress (false) Whether to show the progress percentage.
|
|
27
|
+
# @option props [Boolean] :label_text (false) Whether to show the text label.
|
|
28
|
+
# @option props [Symbol] :progress_label_position (:inside) Position of progress label (:inside or :outside).
|
|
29
|
+
# @option props [Symbol] :text_label_position (:outside) Position of text label (:inside or :outside).
|
|
30
|
+
# @option props [Hash] :label_html ({}) HTML attributes for label elements. Supports :remove_class.
|
|
31
|
+
# @option props [Boolean] :stimulus (false) Whether to add Stimulus controller data attributes for JavaScript interactions.
|
|
32
|
+
# @option props [String] :remove_class ('') CSS classes to remove from the default class list.
|
|
33
|
+
# @option props [Hash] **props Remaining options as HTML attributes.
|
|
34
|
+
#
|
|
35
|
+
# @return [Fluxbit::ProgressComponent]
|
|
36
|
+
def initialize(**props)
|
|
37
|
+
super
|
|
38
|
+
@props = props
|
|
39
|
+
|
|
40
|
+
# Use options() function with config defaults
|
|
41
|
+
@progress = options(@props.delete(:progress), default: @@progress)
|
|
42
|
+
@color = options(@props.delete(:color), collection: styles[:bar][:colors].keys, default: @@color)
|
|
43
|
+
@size = options(@props.delete(:size), default: @@size)
|
|
44
|
+
@text_label = options(@props.delete(:text_label), default: @@text_label)
|
|
45
|
+
@label_progress = options(@props.delete(:label_progress), default: @@label_progress)
|
|
46
|
+
@label_text = options(@props.delete(:label_text), default: @@label_text)
|
|
47
|
+
@progress_label_position = options(@props.delete(:progress_label_position),
|
|
48
|
+
collection: [ :inside, :outside ],
|
|
49
|
+
default: @@progress_label_position)
|
|
50
|
+
@text_label_position = options(@props.delete(:text_label_position),
|
|
51
|
+
collection: [ :inside, :outside ],
|
|
52
|
+
default: @@text_label_position)
|
|
53
|
+
@label_html = options(@props.delete(:label_html), default: @@label_html)
|
|
54
|
+
@stimulus = options(@props.delete(:stimulus), default: @@stimulus)
|
|
55
|
+
|
|
56
|
+
# Sanitize progress value
|
|
57
|
+
@progress = [ @progress.to_i, 0 ].max
|
|
58
|
+
@progress = [ @progress, 100 ].min
|
|
59
|
+
|
|
60
|
+
# Apply styling
|
|
61
|
+
declare_classes
|
|
62
|
+
|
|
63
|
+
# Handle class removal
|
|
64
|
+
@props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def call
|
|
68
|
+
content_tag(:div) do
|
|
69
|
+
safe_join([
|
|
70
|
+
render_outside_labels,
|
|
71
|
+
render_progress_container
|
|
72
|
+
].compact)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def declare_classes
|
|
79
|
+
add(class: styles[:base], to: @props, first_element: true)
|
|
80
|
+
add(class: styles[:sizes][@size], to: @props, first_element: true)
|
|
81
|
+
|
|
82
|
+
# Add Stimulus controller attributes if enabled
|
|
83
|
+
if @stimulus
|
|
84
|
+
@props["data-controller"] = [ @props["data-controller"], "fx-progress" ].compact.join(" ")
|
|
85
|
+
@props["data-fx-progress-progress-value"] = @progress
|
|
86
|
+
@props["data-fx-progress-animate-value"] = true
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def render_outside_labels
|
|
91
|
+
return unless has_outside_labels?
|
|
92
|
+
|
|
93
|
+
content_tag(:div, class: styles[:labels][:outside][:base]) do
|
|
94
|
+
safe_join([
|
|
95
|
+
render_outside_text_label,
|
|
96
|
+
render_outside_progress_label
|
|
97
|
+
].compact)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def render_outside_text_label
|
|
102
|
+
return unless @label_text && @text_label.present? && @text_label_position == :outside
|
|
103
|
+
|
|
104
|
+
label_props = build_label_props(styles[:labels][:outside][:text], is_progress_label: false)
|
|
105
|
+
content_tag(:span, @text_label, **label_props)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def render_outside_progress_label
|
|
109
|
+
return unless @label_progress && @progress_label_position == :outside
|
|
110
|
+
|
|
111
|
+
label_props = build_label_props(styles[:labels][:outside][:progress], is_progress_label: true)
|
|
112
|
+
content_tag(:span, "#{@progress}%", **label_props)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def render_progress_container
|
|
116
|
+
content_tag(:div, **@props) do
|
|
117
|
+
content_tag(:div, progress_bar_content, **progress_bar_props)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def progress_bar_props
|
|
122
|
+
bar_props = {
|
|
123
|
+
class: progress_bar_classes,
|
|
124
|
+
style: "width: #{@progress}%"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Add Stimulus target if enabled
|
|
128
|
+
if @stimulus
|
|
129
|
+
bar_props["data-fx-progress-target"] = "bar"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
bar_props
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def progress_bar_classes
|
|
136
|
+
classes = [ styles[:bar][:base] ]
|
|
137
|
+
classes << styles[:bar][:colors][@color]
|
|
138
|
+
classes << styles[:bar][:text_sizes][@size] if has_inside_labels?
|
|
139
|
+
classes.join(" ")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def progress_bar_content
|
|
143
|
+
return "" unless has_inside_labels?
|
|
144
|
+
|
|
145
|
+
safe_join([
|
|
146
|
+
render_inside_text_label,
|
|
147
|
+
render_inside_progress_label
|
|
148
|
+
].compact, " ")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def render_inside_text_label
|
|
152
|
+
return unless @label_text && @text_label.present? && @text_label_position == :inside
|
|
153
|
+
|
|
154
|
+
label_props = build_label_props(styles[:labels][:inside][:text], is_progress_label: false)
|
|
155
|
+
content_tag(:span, @text_label, **label_props)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def render_inside_progress_label
|
|
159
|
+
return unless @label_progress && @progress_label_position == :inside
|
|
160
|
+
|
|
161
|
+
label_props = build_label_props(styles[:labels][:inside][:progress], is_progress_label: true)
|
|
162
|
+
content_tag(:span, "#{@progress}%", **label_props)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def has_outside_labels?
|
|
166
|
+
(@label_text && @text_label.present? && @text_label_position == :outside) ||
|
|
167
|
+
(@label_progress && @progress_label_position == :outside)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def has_inside_labels?
|
|
171
|
+
(@label_text && @text_label.present? && @text_label_position == :inside) ||
|
|
172
|
+
(@label_progress && @progress_label_position == :inside)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def build_label_props(base_class, is_progress_label: false)
|
|
176
|
+
label_props = @label_html.dup
|
|
177
|
+
|
|
178
|
+
# Start with base class and merge with any custom classes
|
|
179
|
+
label_props[:class] = base_class
|
|
180
|
+
add(class: @label_html[:class], to: label_props) if @label_html[:class].present?
|
|
181
|
+
|
|
182
|
+
# Add Stimulus target if enabled and this is a progress label
|
|
183
|
+
if @stimulus && is_progress_label
|
|
184
|
+
label_props["data-fx-progress-target"] = "progressLabel"
|
|
185
|
+
elsif @stimulus && !is_progress_label
|
|
186
|
+
label_props["data-fx-progress-target"] = "textLabel"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Handle remove_class for labels
|
|
190
|
+
if label_props[:remove_class].present?
|
|
191
|
+
label_props[:class] = remove_class(label_props.delete(:remove_class), label_props[:class])
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
label_props
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# The `Fluxbit::SkeletonComponent` is a customizable skeleton loading component that extends `Fluxbit::Component`.
|
|
5
|
+
# It provides animated placeholders for content that is loading, supporting various types like text, images, cards,
|
|
6
|
+
# avatars, and more complex layouts.
|
|
7
|
+
#
|
|
8
|
+
# @example Basic usage
|
|
9
|
+
# = fx_skeleton
|
|
10
|
+
# = fx_skeleton(variant: :image)
|
|
11
|
+
# = fx_skeleton(variant: :card)
|
|
12
|
+
#
|
|
13
|
+
# @see docs/02_Components/Skeleton.md For detailed documentation.
|
|
14
|
+
class Fluxbit::SkeletonComponent < Fluxbit::Component
|
|
15
|
+
include Fluxbit::Config::SkeletonComponent
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# Initializes the skeleton component with the given properties.
|
|
19
|
+
#
|
|
20
|
+
# @param [Hash] **props The properties to customize the skeleton.
|
|
21
|
+
# @option props [Symbol] :variant (:default) The type of skeleton (:default, :text, :image, :video, :avatar, :card, :widget, :list, :testimonial, :button).
|
|
22
|
+
# @option props [Boolean] :animation (true) Whether to show the pulse animation.
|
|
23
|
+
# @option props [Integer] :rows (3) Number of text rows for default/text variants.
|
|
24
|
+
# @option props [Symbol] :size (:medium) Size for avatar, image, and video variants (:small, :medium, :large).
|
|
25
|
+
# @option props [Integer] :lines (nil) Number of lines for text-based variants.
|
|
26
|
+
# @option props [String] :remove_class ('') CSS classes to remove from the default class list.
|
|
27
|
+
# @option props [Hash] **props Remaining options declared as HTML attributes.
|
|
28
|
+
#
|
|
29
|
+
# @return [Fluxbit::SkeletonComponent]
|
|
30
|
+
def initialize(**props)
|
|
31
|
+
super
|
|
32
|
+
@props = props
|
|
33
|
+
|
|
34
|
+
@variant = options(@props.delete(:variant),
|
|
35
|
+
collection: [ :default, :text, :image, :video, :avatar, :card, :widget, :list, :testimonial, :button ],
|
|
36
|
+
default: @@variant)
|
|
37
|
+
@animation = options(@props.delete(:animation), default: @@animation)
|
|
38
|
+
@rows = options(@props.delete(:rows), default: @@rows).to_i
|
|
39
|
+
@size = options(@props.delete(:size), collection: [ :small, :medium, :large ], default: :medium)
|
|
40
|
+
@lines = @props.delete(:lines)&.to_i
|
|
41
|
+
|
|
42
|
+
declare_classes
|
|
43
|
+
@props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
|
|
44
|
+
@props[:role] = "status"
|
|
45
|
+
@props["aria-label"] = "Loading"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def call
|
|
49
|
+
tag.div(**@props) do
|
|
50
|
+
case @variant
|
|
51
|
+
when :default, :text
|
|
52
|
+
render_text_skeleton
|
|
53
|
+
when :image
|
|
54
|
+
render_image_skeleton
|
|
55
|
+
when :video
|
|
56
|
+
render_video_skeleton
|
|
57
|
+
when :avatar
|
|
58
|
+
render_avatar_skeleton
|
|
59
|
+
when :card
|
|
60
|
+
render_card_skeleton
|
|
61
|
+
when :widget
|
|
62
|
+
render_widget_skeleton
|
|
63
|
+
when :list
|
|
64
|
+
render_list_skeleton
|
|
65
|
+
when :testimonial
|
|
66
|
+
render_testimonial_skeleton
|
|
67
|
+
when :button
|
|
68
|
+
render_button_skeleton
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def declare_classes
|
|
76
|
+
base_classes = []
|
|
77
|
+
base_classes << styles[:base] if @animation
|
|
78
|
+
base_classes << styles[:container]
|
|
79
|
+
|
|
80
|
+
add(class: base_classes.join(" "), to: @props, first_element: true)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def render_text_skeleton
|
|
84
|
+
lines_count = @lines || @rows
|
|
85
|
+
content = []
|
|
86
|
+
|
|
87
|
+
lines_count.times do |index|
|
|
88
|
+
width_class = case index
|
|
89
|
+
when 0 then styles[:widths][:first_line]
|
|
90
|
+
when lines_count - 1 then styles[:widths][:last_line]
|
|
91
|
+
else styles[:widths][:full]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
content << tag.div(class: "#{styles[:text][:line]} #{width_class} #{styles[:spacing][:small]}")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
|
|
98
|
+
safe_join(content)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def render_image_skeleton
|
|
102
|
+
content = []
|
|
103
|
+
content << tag.div(class: "#{styles[:image][:container]} #{styles[:image][@size]}") do
|
|
104
|
+
concat(
|
|
105
|
+
tag.svg(class: styles[:image][:icon],
|
|
106
|
+
"aria-hidden": "true",
|
|
107
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
108
|
+
fill: "currentColor",
|
|
109
|
+
viewBox: "0 0 20 18") do
|
|
110
|
+
concat(
|
|
111
|
+
tag.path(d: "M18 0H2a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2Zm-5.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm4.376 10.481A1 1 0 0 1 16 15H4a1 1 0 0 1-.895-1.447l3.5-7A1 1 0 0 1 7.468 6a.965.965 0 0 1 .9.5l2.775 4.757 1.546-1.887a1 1 0 0 1 1.618.1l2.541 4a1 1 0 0 1 .028 1.011Z")
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
|
|
117
|
+
safe_join(content)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def render_video_skeleton
|
|
121
|
+
content = []
|
|
122
|
+
content << tag.div(class: "#{styles[:video][:container]} #{styles[:video][@size]}") do
|
|
123
|
+
concat(
|
|
124
|
+
tag.svg(class: styles[:video][:icon],
|
|
125
|
+
"aria-hidden": "true",
|
|
126
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
127
|
+
fill: "currentColor",
|
|
128
|
+
viewBox: "0 0 16 20") do
|
|
129
|
+
concat(
|
|
130
|
+
tag.path(d: "M5 5V.13a2.96 2.96 0 0 0-1.293.749L.879 3.707A2.98 2.98 0 0 0 .13 5H5Z")
|
|
131
|
+
)
|
|
132
|
+
concat(
|
|
133
|
+
tag.path(d: "M14.066 0H7v5a2 2 0 0 1-2 2H0v11a1.97 1.97 0 0 0 1.934 2h12.132A1.97 1.97 0 0 0 16 18V2a1.97 1.97 0 0 0-1.934-2ZM9 13a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2Zm4 .382a1 1 0 0 1-1.447.894L10 13v-2l1.553-1.276a1 1 0 0 1 1.447.894v2.764Z")
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
|
|
139
|
+
safe_join(content)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def render_avatar_skeleton
|
|
143
|
+
content = []
|
|
144
|
+
content << tag.div(class: styles[:avatar][@size])
|
|
145
|
+
content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
|
|
146
|
+
safe_join(content)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def render_card_skeleton
|
|
150
|
+
tag.div(class: styles[:card][:container]) do
|
|
151
|
+
content = []
|
|
152
|
+
content << tag.div(class: "#{styles[:card][:header]} #{styles[:spacing][:medium]} #{styles[:widths][:card_header]}")
|
|
153
|
+
|
|
154
|
+
lines_count = @lines || @rows
|
|
155
|
+
lines_count.times do |index|
|
|
156
|
+
width_class = index == lines_count - 1 ? styles[:widths][:last_line] : styles[:widths][:full]
|
|
157
|
+
spacing_class = index < lines_count - 1 ? styles[:spacing][:small] : ""
|
|
158
|
+
content << tag.div(class: "#{styles[:card][:body]} #{spacing_class} #{width_class}")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
|
|
162
|
+
safe_join(content)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def render_widget_skeleton
|
|
167
|
+
tag.div(class: styles[:widget][:container]) do
|
|
168
|
+
content = []
|
|
169
|
+
content << tag.div(class: "#{styles[:widget][:title]} #{styles[:widths][:widget_title]}")
|
|
170
|
+
|
|
171
|
+
lines_count = @lines || @rows
|
|
172
|
+
lines_count.times do |index|
|
|
173
|
+
width_class = index == lines_count - 1 ? styles[:widths][:last_line] : styles[:widths][:full]
|
|
174
|
+
spacing_class = index < lines_count - 1 ? styles[:spacing][:small] : ""
|
|
175
|
+
content << tag.div(class: "#{styles[:widget][:content]} #{spacing_class} #{width_class}")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
|
|
179
|
+
safe_join(content)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def render_list_skeleton
|
|
184
|
+
tag.div(class: styles[:list][:container]) do
|
|
185
|
+
content = []
|
|
186
|
+
|
|
187
|
+
lines_count = @lines || @rows
|
|
188
|
+
lines_count.times do
|
|
189
|
+
content << tag.div(class: styles[:list][:item]) do
|
|
190
|
+
concat(tag.div(class: styles[:list][:avatar]))
|
|
191
|
+
concat(
|
|
192
|
+
tag.div(class: styles[:list][:content]) do
|
|
193
|
+
concat(tag.div(class: "#{styles[:text][:line]} #{styles[:widths][:list_name]} #{styles[:spacing][:small]}"))
|
|
194
|
+
concat(tag.div(class: "#{styles[:text][:small]} #{styles[:widths][:list_content]}"))
|
|
195
|
+
end
|
|
196
|
+
)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
|
|
201
|
+
safe_join(content)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def render_testimonial_skeleton
|
|
206
|
+
tag.div(class: styles[:testimonial][:container]) do
|
|
207
|
+
content = []
|
|
208
|
+
|
|
209
|
+
lines_count = @lines || @rows
|
|
210
|
+
lines_count.times do |index|
|
|
211
|
+
width = index == lines_count - 1 ? styles[:widths][:testimonial_last] : styles[:widths][:full]
|
|
212
|
+
spacing_class = index < lines_count - 1 ? styles[:spacing][:small] : ""
|
|
213
|
+
content << tag.div(class: "#{styles[:testimonial][:quote]} #{width} #{spacing_class}")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
content << tag.div(class: styles[:testimonial][:author]) do
|
|
217
|
+
concat(tag.div(class: styles[:testimonial][:avatar]))
|
|
218
|
+
concat(
|
|
219
|
+
tag.div do
|
|
220
|
+
concat(tag.div(class: "#{styles[:text][:line]} #{styles[:widths][:testimonial_name]} #{styles[:spacing][:small]}"))
|
|
221
|
+
concat(tag.div(class: "#{styles[:text][:small]} #{styles[:widths][:testimonial_title]}"))
|
|
222
|
+
end
|
|
223
|
+
)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
|
|
227
|
+
safe_join(content)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def render_button_skeleton
|
|
232
|
+
content = []
|
|
233
|
+
content << tag.div(class: styles[:button])
|
|
234
|
+
content << tag.span(t("fluxbit.skeleton.loading"), class: styles[:screen_reader])
|
|
235
|
+
safe_join(content)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<% if @text_outside && @text %>
|
|
2
|
+
<div class="<%= styles[:action][:text][:base] %>">
|
|
3
|
+
<span class="<%= text_classes %>"><%= @text %></span>
|
|
4
|
+
<% if @href %>
|
|
5
|
+
<%= link_to(@href, **@props) do %>
|
|
6
|
+
<%= icon @icon, class: icon_classes if @icon %>
|
|
7
|
+
<%= content if content.present? %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% else %>
|
|
10
|
+
<%= tag.button(**@props) do %>
|
|
11
|
+
<%= icon @icon, class: icon_classes if @icon %>
|
|
12
|
+
<%= content if content.present? %>
|
|
13
|
+
<% end %>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
<% else %>
|
|
17
|
+
<% if @href %>
|
|
18
|
+
<%= link_to(@href, **@props) do %>
|
|
19
|
+
<%= icon @icon, class: icon_classes if @icon %>
|
|
20
|
+
<%= content if content.present? %>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% else %>
|
|
23
|
+
<%= tag.button(**@props) do %>
|
|
24
|
+
<%= icon @icon, class: icon_classes if @icon %>
|
|
25
|
+
<%= content if content.present? %>
|
|
26
|
+
<% end %>
|
|
27
|
+
<% end %>
|
|
28
|
+
<% end %>
|
|
29
|
+
|
|
30
|
+
<%= render_popover_or_tooltip %>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The `Fluxbit::SpeedDialActionComponent` is a component for rendering individual action items in a Speed Dial.
|
|
4
|
+
class Fluxbit::SpeedDialActionComponent < Fluxbit::Component
|
|
5
|
+
include Fluxbit::Config::SpeedDialComponent
|
|
6
|
+
|
|
7
|
+
# Initializes the SpeedDialAction component.
|
|
8
|
+
#
|
|
9
|
+
# @param [Hash] **props The properties to customize the speed dial action.
|
|
10
|
+
# @option props [String, Symbol] :icon (nil) The icon to display in the action button.
|
|
11
|
+
# @option props [String] :text (nil) The text label for the action.
|
|
12
|
+
# @option props [String] :tooltip (nil) The tooltip text (defaults to text if not provided).
|
|
13
|
+
# @option props [String] :href (nil) The URL to link to (creates an anchor tag instead of button).
|
|
14
|
+
# @option props [Boolean] :text_outside (false) Whether to display text outside the button.
|
|
15
|
+
# @option props [Boolean] :square (false) Whether to use square shape instead of rounded.
|
|
16
|
+
# @option props [Symbol] :tooltip_placement (:left) Tooltip placement.
|
|
17
|
+
# @option props [String] :remove_class ('') CSS classes to remove from the default class list.
|
|
18
|
+
# @option props [Hash] **props Remaining options declared as HTML attributes.
|
|
19
|
+
#
|
|
20
|
+
# @return [Fluxbit::SpeedDialActionComponent]
|
|
21
|
+
def initialize(**props)
|
|
22
|
+
super(**props.slice(:tooltip_text, :tooltip_placement, :tooltip_trigger))
|
|
23
|
+
@props = props
|
|
24
|
+
|
|
25
|
+
@icon = @props.delete(:icon)
|
|
26
|
+
@text = @props.delete(:text)
|
|
27
|
+
@tooltip = @props.delete(:tooltip) || @text
|
|
28
|
+
@href = @props.delete(:href)
|
|
29
|
+
@text_outside = options @props.delete(:text_outside), default: false
|
|
30
|
+
@square = options @props.delete(:square), default: false
|
|
31
|
+
@tooltip_placement = options @props.delete(:tooltip_placement), default: :left
|
|
32
|
+
@props[:id] ||= "speed-dial-action-#{random_id}"
|
|
33
|
+
|
|
34
|
+
# Set tooltip via parent component if provided
|
|
35
|
+
@tooltip_text = @tooltip if @tooltip.present?
|
|
36
|
+
|
|
37
|
+
add class: [
|
|
38
|
+
styles[:action][:base],
|
|
39
|
+
@square ? styles[:action][:shapes][:square] : styles[:action][:shapes][:rounded]
|
|
40
|
+
], to: @props
|
|
41
|
+
@props[:type] ||= "button" unless @href
|
|
42
|
+
|
|
43
|
+
remove_class_from_props(@props)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def before_render
|
|
47
|
+
add_popover_or_tooltip
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def icon_classes
|
|
53
|
+
styles[:action][:icon]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def text_classes
|
|
57
|
+
styles[:action][:text][:outside]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<div <%= tag.attributes(**@props) %> data-dial-init>
|
|
2
|
+
<% if top_position? %>
|
|
3
|
+
<button type="button" data-dial-toggle="<%= menu_id %>" aria-controls="<%= menu_id %>" aria-expanded="false" class="<%= classes trigger_classes %>">
|
|
4
|
+
<% if @trigger_icon.nil? %>
|
|
5
|
+
<%= plus_icon(class: styles[:trigger][:icon]) %>
|
|
6
|
+
<% else %>
|
|
7
|
+
<%= icon @trigger_icon, class: styles[:trigger][:icon] %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<span class="<%= styles[:screen_reader] %>"><%= t("fluxbit.speed_dial.open_actions_menu") %></span>
|
|
10
|
+
</button>
|
|
11
|
+
|
|
12
|
+
<div id="<%= menu_id %>" class="<%= classes menu_classes %>">
|
|
13
|
+
<% actions.each do |action| %>
|
|
14
|
+
<%= action %>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
<% else %>
|
|
18
|
+
<div id="<%= menu_id %>" class="<%= classes menu_classes %>">
|
|
19
|
+
<% actions.each do |action| %>
|
|
20
|
+
<%= action %>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<button type="button" data-dial-toggle="<%= menu_id %>" aria-controls="<%= menu_id %>" aria-expanded="false" class="<%= classes trigger_classes %>">
|
|
25
|
+
<% if @trigger_icon.nil? %>
|
|
26
|
+
<%= plus_icon(class: styles[:trigger][:icon]) %>
|
|
27
|
+
<% else %>
|
|
28
|
+
<%= icon @trigger_icon, class: styles[:trigger][:icon] %>
|
|
29
|
+
<% end %>
|
|
30
|
+
<span class="<%= styles[:screen_reader] %>"><%= t("fluxbit.speed_dial.open_actions_menu") %></span>
|
|
31
|
+
</button>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|