flowbite-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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/README.md +3 -5
  4. data/app/components/flowbite/alert/alert.html.erb +36 -0
  5. data/app/components/flowbite/alert/icon.rb +53 -0
  6. data/app/components/flowbite/alert.rb +156 -0
  7. data/app/components/flowbite/avatar/avatar.html.erb +23 -0
  8. data/app/components/flowbite/avatar/dot.rb +85 -0
  9. data/app/components/flowbite/avatar/placeholder_icon.html.erb +13 -0
  10. data/app/components/flowbite/avatar/placeholder_icon.rb +44 -0
  11. data/app/components/flowbite/avatar.rb +132 -0
  12. data/app/components/flowbite/badge/badge.html.erb +7 -0
  13. data/app/components/flowbite/badge/dot.rb +45 -0
  14. data/app/components/flowbite/badge/pill.rb +40 -0
  15. data/app/components/flowbite/badge.rb +116 -0
  16. data/app/components/flowbite/card.rb +4 -8
  17. data/app/components/flowbite/input_field/checkbox.rb +7 -7
  18. data/app/components/flowbite/input_field/input_field.html.erb +1 -0
  19. data/app/components/flowbite/input_field/select.rb +1 -1
  20. data/app/components/flowbite/input_field.rb +29 -22
  21. data/app/components/flowbite/pagination/chevron_left_icon.rb +34 -0
  22. data/app/components/flowbite/pagination/chevron_right_icon.rb +34 -0
  23. data/app/components/flowbite/pagination/gap.rb +24 -0
  24. data/app/components/flowbite/pagination/link.rb +52 -0
  25. data/app/components/flowbite/pagination/next_link.rb +67 -0
  26. data/app/components/flowbite/pagination/pagination.html.erb +11 -0
  27. data/app/components/flowbite/pagination/previous_link.rb +67 -0
  28. data/app/components/flowbite/pagination.rb +63 -0
  29. data/app/components/flowbite/progress/progress.html.erb +14 -0
  30. data/app/components/flowbite/progress.rb +163 -0
  31. data/app/components/flowbite/rating/rating.html.erb +13 -0
  32. data/app/components/flowbite/rating/star.html.erb +13 -0
  33. data/app/components/flowbite/rating/star.rb +59 -0
  34. data/app/components/flowbite/rating.rb +64 -0
  35. data/app/components/flowbite/sidebar/item.rb +68 -0
  36. data/app/components/flowbite/sidebar/navigation.rb +62 -0
  37. data/app/components/flowbite/sidebar.rb +62 -0
  38. data/app/components/flowbite/spinner/spinner.html.erb +20 -0
  39. data/app/components/flowbite/spinner.rb +66 -0
  40. data/app/components/flowbite/toast/close_button.html.erb +17 -0
  41. data/app/components/flowbite/toast/close_button.rb +32 -0
  42. data/app/components/flowbite/toast/icon.html.erb +8 -2
  43. data/app/components/flowbite/toast/toast.html.erb +7 -34
  44. data/app/components/flowbite/toast.rb +39 -1
  45. data/lib/flowbite/components/version.rb +1 -1
  46. metadata +39 -3
@@ -0,0 +1,14 @@
1
+ <% if label_outside? %>
2
+ <div class="flex justify-between mb-1">
3
+ <span class="text-sm font-medium text-body"><%= label %></span>
4
+ <span class="text-sm font-medium text-body"><%= percentage %></span>
5
+ </div>
6
+ <% end %>
7
+
8
+ <%= tag.div(class: classes.join(" "), **track_options) do %>
9
+ <div class="<%= bar_classes.join(" ") %>" style="<%= width_style %>">
10
+ <% if label_inside? %>
11
+ <%= percentage %>
12
+ <% end %>
13
+ </div>
14
+ <% end %>
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ # Renders a progress bar indicating the completion of a task.
5
+ #
6
+ # The progress value is clamped to the range 0 to 100 and used as the
7
+ # width of the bar.
8
+ #
9
+ # The track is rendered with role="progressbar" and the matching ARIA
10
+ # value attributes. The +label+, if given, is used as its accessible name.
11
+ # Pass +aria: {label: "..."}+ to name a progress bar without a visible
12
+ # label.
13
+ #
14
+ # @example Basic usage
15
+ # <%= render(Flowbite::Progress.new(progress: 45)) %>
16
+ #
17
+ # @example With a custom style and size
18
+ # <%= render(Flowbite::Progress.new(progress: 45, size: :lg, style: :success)) %>
19
+ #
20
+ # @example With the label inside the bar
21
+ # <%= render(Flowbite::Progress.new(label_position: :inside, progress: 45)) %>
22
+ #
23
+ # @example With a label outside the bar
24
+ # <%= render(Flowbite::Progress.new(label: "Flowbite", label_position: :outside, progress: 45)) %>
25
+ #
26
+ # @example With an accessible name and no visible label
27
+ # <%= render(Flowbite::Progress.new(aria: {label: "Upload progress"}, progress: 45)) %>
28
+ #
29
+ # @see https://flowbite.com/docs/components/progress/
30
+ # @lookbook_embed ProgressPreview
31
+ class Progress < ViewComponent::Base
32
+ LABEL_POSITIONS = [:inside, :none, :outside].freeze
33
+
34
+ SIZES = {
35
+ sm: ["h-1.5"],
36
+ default: ["h-2"],
37
+ lg: ["h-2.5"]
38
+ }.freeze
39
+
40
+ # Only vertical padding, so the bar can shrink to zero width at 0%.
41
+ INSIDE_LABEL_CLASSES = [
42
+ "text-xs", "font-medium", "text-white", "text-center", "py-0.5",
43
+ "leading-none", "rounded-full", "h-4", "flex", "items-center",
44
+ "justify-center"
45
+ ].freeze
46
+
47
+ class << self
48
+ # @return [Array<String>] The CSS classes for the bar indicating the
49
+ # progress.
50
+ def bar_classes(label_position: :none, size: :default, style: :brand)
51
+ style = styles.fetch(style)
52
+ if label_position == :inside
53
+ style.fetch(:default) + INSIDE_LABEL_CLASSES
54
+ else
55
+ style.fetch(:default) + sizes.fetch(size) + ["rounded-full"]
56
+ end
57
+ end
58
+
59
+ # @return [Array<String>] The CSS classes for the track containing the
60
+ # bar.
61
+ def classes(label_position: :none, size: :default)
62
+ result = ["w-full", "bg-neutral-quaternary", "rounded-full"]
63
+ result += sizes.fetch(size) unless label_position == :inside
64
+ result
65
+ end
66
+
67
+ def sizes
68
+ SIZES
69
+ end
70
+
71
+ def styles
72
+ Flowbite::Styles.from_hash(
73
+ {
74
+ brand: {default: ["bg-brand"]},
75
+ danger: {default: ["bg-danger"]},
76
+ dark: {default: ["bg-dark"]},
77
+ success: {default: ["bg-success"]},
78
+ warning: {default: ["bg-warning"]}
79
+ }.freeze
80
+ )
81
+ end
82
+ end
83
+
84
+ attr_reader :label, :label_position, :options
85
+
86
+ # @param class [String, Array<String>] Additional CSS classes for the
87
+ # track element.
88
+ # @param label [String] Text to show next to the percentage when
89
+ # label_position is :outside. Also used as the accessible name of the
90
+ # progress bar.
91
+ # @param label_position [Symbol] Where to show the percentage (:inside,
92
+ # :none, :outside).
93
+ # @param options [Hash] Additional HTML options for the track element.
94
+ # @param progress [Numeric] The progress in percent. Values outside the
95
+ # range 0 to 100 are clamped.
96
+ # @param size [Symbol] The height of the progress bar (:sm, :default,
97
+ # :lg). Ignored when label_position is :inside.
98
+ # @param style [Symbol] The color style of the bar (:brand, :danger,
99
+ # :dark, :success, :warning).
100
+ def initialize(progress:, class: nil, label: nil, label_position: :none, size: :default, style: :brand, **options)
101
+ unless LABEL_POSITIONS.include?(label_position)
102
+ raise ArgumentError,
103
+ "Unknown label_position: #{label_position}. " \
104
+ "Available label positions: #{LABEL_POSITIONS.join(", ")}"
105
+ end
106
+
107
+ @class = Array.wrap(binding.local_variable_get(:class))
108
+ @label = label
109
+ @label_position = label_position
110
+ @options = options
111
+ @progress = progress
112
+ @size = size
113
+ @style = style
114
+ end
115
+
116
+ # @return [Array<String>] The CSS classes for the bar.
117
+ def bar_classes
118
+ self.class.bar_classes(label_position: label_position, size: @size, style: @style)
119
+ end
120
+
121
+ # @return [Array<String>] The CSS classes for the track.
122
+ def classes
123
+ self.class.classes(label_position: label_position, size: @size) + @class
124
+ end
125
+
126
+ def label_inside?
127
+ label_position == :inside
128
+ end
129
+
130
+ def label_outside?
131
+ label_position == :outside
132
+ end
133
+
134
+ # @return [String] The progress formatted as a percentage, ie "45%".
135
+ def percentage
136
+ "#{rounded_progress}%"
137
+ end
138
+
139
+ # @return [Numeric] The progress clamped to the range 0 to 100.
140
+ def progress
141
+ @progress.clamp(0, 100)
142
+ end
143
+
144
+ # @return [Numeric] The clamped progress rounded to one decimal place.
145
+ # Whole numbers are returned as Integers, ie 45 rather than 45.0.
146
+ def rounded_progress
147
+ value = progress.to_f.round(1)
148
+ (value == value.to_i) ? value.to_i : value
149
+ end
150
+
151
+ # @return [Hash] The HTML attributes for the track element, including
152
+ # the progressbar role and ARIA values. Merged with the given options.
153
+ def track_options
154
+ aria = {label: label.presence, valuemax: 100, valuemin: 0, valuenow: rounded_progress}.compact
155
+ {aria: aria, role: "progressbar"}.deep_merge(options)
156
+ end
157
+
158
+ # @return [String] The inline style setting the width of the bar.
159
+ def width_style
160
+ "width: #{percentage}"
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,13 @@
1
+ <%= tag.div(class: container_classes.join(" "), **options) do %>
2
+ <div class="flex items-center space-x-1">
3
+ <% stars.each do |filled| %>
4
+ <%= render(Flowbite::Rating::Star.new(filled: filled, size: size)) %>
5
+ <% end %>
6
+ </div>
7
+
8
+ <span class="sr-only"><%= screen_reader_text %></span>
9
+
10
+ <% if text.present? %>
11
+ <p class="ms-2 text-sm font-medium text-body"><%= text %></p>
12
+ <% end %>
13
+ <% end %>
@@ -0,0 +1,13 @@
1
+ <svg
2
+ aria-hidden="true"
3
+ class="<%= classes.join(" ") %>"
4
+ fill="currentColor"
5
+ height="24"
6
+ viewBox="0 0 24 24"
7
+ width="24"
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ >
10
+ <path
11
+ d="M13.849 4.22c-.684-1.626-3.014-1.626-3.698 0L8.397 8.387l-4.552.361c-1.775.14-2.495 2.331-1.142 3.477l3.468 2.937-1.06 4.392c-.413 1.713 1.472 3.067 2.992 2.149L12 19.35l3.897 2.354c1.52.918 3.405-.436 2.992-2.15l-1.06-4.39 3.468-2.938c1.353-1.146.633-3.336-1.142-3.477l-4.552-.36-1.754-4.17Z"
12
+ />
13
+ </svg>
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ class Rating
5
+ # Renders a single star of a rating.
6
+ #
7
+ # @param filled [Boolean] Whether the star is filled (default: true).
8
+ # @param size [Symbol] The size of the star (:default, :md, :lg).
9
+ #
10
+ # @see https://flowbite.com/docs/components/rating/
11
+ class Star < ViewComponent::Base
12
+ SIZES = {
13
+ default: ["w-5", "h-5"],
14
+ md: ["w-6", "h-6"],
15
+ lg: ["w-7", "h-7"]
16
+ }.freeze
17
+
18
+ class << self
19
+ def classes(size: :default, state: :filled, style: :default)
20
+ sizes.fetch(size) + styles.fetch(style).fetch(state)
21
+ end
22
+
23
+ def sizes
24
+ SIZES
25
+ end
26
+
27
+ def styles
28
+ Flowbite::Styles.from_hash(
29
+ {
30
+ default: {
31
+ empty: ["text-fg-disabled"],
32
+ filled: ["text-fg-yellow"]
33
+ }
34
+ }.freeze
35
+ )
36
+ end
37
+ end
38
+
39
+ attr_reader :size
40
+
41
+ def initialize(filled: true, size: :default)
42
+ @filled = filled
43
+ @size = size
44
+ end
45
+
46
+ def classes
47
+ self.class.classes(size: size, state: state)
48
+ end
49
+
50
+ def filled?
51
+ !!@filled
52
+ end
53
+
54
+ def state
55
+ filled? ? :filled : :empty
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ # Renders a star rating, showing a value out of a maximum number of stars.
5
+ #
6
+ # The number of filled stars is the value rounded to the nearest whole
7
+ # number (halves round up), clamped between 0 and +max+. The remaining stars
8
+ # are rendered as empty stars. The exact value is announced to screen
9
+ # readers.
10
+ #
11
+ # @example Basic usage
12
+ # <%= render(Flowbite::Rating.new(value: 4)) %>
13
+ #
14
+ # @example With text and a custom size
15
+ # <%= render(Flowbite::Rating.new(size: :lg, text: "4.95 out of 5", value: 4.95)) %>
16
+ #
17
+ # @see https://flowbite.com/docs/components/rating/
18
+ # @lookbook_embed RatingPreview
19
+ class Rating < ViewComponent::Base
20
+ class << self
21
+ def classes
22
+ ["flex", "items-center"]
23
+ end
24
+ end
25
+
26
+ attr_reader :max, :options, :size, :text, :value
27
+
28
+ # @param class [String, Array<String>] Additional CSS classes for the
29
+ # container element.
30
+ # @param max [Integer] The total number of stars to render (default: 5).
31
+ # @param options [Hash] Additional HTML options for the container element.
32
+ # @param size [Symbol] The size of the stars (:default, :md, :lg).
33
+ # @param text [String] Optional text rendered after the stars, e.g.
34
+ # "4.95 out of 5".
35
+ # @param value [Numeric] The rating value, e.g. 4 or 4.95.
36
+ def initialize(value:, class: nil, max: 5, size: :default, text: nil, **options)
37
+ @class = Array.wrap(binding.local_variable_get(:class))
38
+ @max = max
39
+ @options = options
40
+ @size = size
41
+ @text = text
42
+ @value = value
43
+ end
44
+
45
+ def container_classes
46
+ self.class.classes + @class
47
+ end
48
+
49
+ # @return [Integer] The number of stars to render as filled.
50
+ def filled_count
51
+ value.round.clamp(0, max)
52
+ end
53
+
54
+ # @return [String] The rating as announced to screen readers.
55
+ def screen_reader_text
56
+ "Rated #{value} out of #{max}"
57
+ end
58
+
59
+ # @return [Array<Boolean>] One entry per star, true if the star is filled.
60
+ def stars
61
+ Array.new(max) { |index| index < filled_count }
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ class Sidebar
5
+ # Renders a sidebar navigation item.
6
+ #
7
+ # Each item renders as a list item containing a link. Optionally, an icon
8
+ # can be provided using the +icon+ slot, which will be displayed before the
9
+ # label text.
10
+ #
11
+ # @example Basic item
12
+ # <%= render Flowbite::Sidebar::Item.new(href: "/dashboard") { "Dashboard" } %>
13
+ #
14
+ # @example Item with icon
15
+ # <%= render(Flowbite::Sidebar::Item.new(href: "/dashboard")) do |item| %>
16
+ # <% item.with_icon do %>
17
+ # <svg class="w-5 h-5" ...>...</svg>
18
+ # <% end %>
19
+ # Dashboard
20
+ # <% end %>
21
+ #
22
+ # @viewcomponent_slot icon An optional icon displayed before the label text.
23
+ class Item < ViewComponent::Base
24
+ renders_one :icon
25
+
26
+ attr_reader :href, :options
27
+
28
+ class << self
29
+ def classes
30
+ [
31
+ "flex", "items-center", "px-2", "py-1.5", "text-body",
32
+ "rounded-base", "hover:bg-neutral-tertiary", "hover:text-fg-brand", "group"
33
+ ]
34
+ end
35
+ end
36
+
37
+ # @param class [Array<String>] Additional CSS classes for the link element.
38
+ # @param href [String] The URL for the navigation link.
39
+ # @param options [Hash] Additional HTML attributes for the link element.
40
+ def initialize(href:, class: nil, **options)
41
+ super()
42
+ @class = Array.wrap(binding.local_variable_get(:class))
43
+ @href = href
44
+ @options = options
45
+ end
46
+
47
+ def call
48
+ content_tag(:li) do
49
+ link_options = {class: link_classes}.merge(options)
50
+ content_tag(:a, href: href, **link_options) do
51
+ concat(icon) if icon?
52
+ concat(content_tag(:span, content, class: label_classes))
53
+ end
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def label_classes
60
+ "ms-3" if icon?
61
+ end
62
+
63
+ def link_classes
64
+ self.class.classes + @class
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ class Sidebar
5
+ # Renders the navigation list for a sidebar.
6
+ #
7
+ # This component renders a +<ul>+ with navigation items. It can be used
8
+ # inside a {Flowbite::Sidebar} for a fixed-position sidebar, or standalone
9
+ # in any layout that needs sidebar-style navigation.
10
+ #
11
+ # @example Inside a Sidebar
12
+ # <%= render(Flowbite::Sidebar.new) do %>
13
+ # <%= render(Flowbite::Sidebar::Navigation.new) do |nav| %>
14
+ # <% nav.with_item do %>
15
+ # <%= render(Flowbite::Sidebar::Item.new(href: "/")) { "Home" } %>
16
+ # <% end %>
17
+ # <% end %>
18
+ # <% end %>
19
+ #
20
+ # @example Standalone
21
+ # <%= render(Flowbite::Sidebar::Navigation.new) do |nav| %>
22
+ # <% nav.with_item do %>
23
+ # <%= render(Flowbite::Sidebar::Item.new(href: "/")) { "Home" } %>
24
+ # <% end %>
25
+ # <% end %>
26
+ class Navigation < ViewComponent::Base
27
+ renders_many :items
28
+
29
+ class << self
30
+ def classes
31
+ ["space-y-2", "font-medium"]
32
+ end
33
+ end
34
+
35
+ # @param class [Array<String>] Additional CSS classes for the list element.
36
+ # @param options [Hash] Additional HTML options for the list element.
37
+ def initialize(class: nil, **options)
38
+ super()
39
+ @class = Array.wrap(binding.local_variable_get(:class))
40
+ @options = options
41
+ end
42
+
43
+ def call
44
+ content_tag(:ul, list_options) do
45
+ items.each do |item|
46
+ concat(item)
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def list_classes
54
+ self.class.classes + @class
55
+ end
56
+
57
+ def list_options
58
+ {class: list_classes}.merge(@options)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ # Renders a fixed-position sidebar container.
5
+ #
6
+ # Use {Flowbite::Sidebar} as the outer shell and
7
+ # {Flowbite::Sidebar::Navigation} inside it to render the list of
8
+ # {Flowbite::Sidebar::Item}s.
9
+ #
10
+ # @example Usage
11
+ # <%= render(Flowbite::Sidebar.new) do %>
12
+ # <%= render(Flowbite::Sidebar::Navigation.new) do |nav| %>
13
+ # <% nav.with_item do %>
14
+ # <%= render(Flowbite::Sidebar::Item.new(href: "/dashboard")) { "Dashboard" } %>
15
+ # <% end %>
16
+ # <% end %>
17
+ # <% end %>
18
+ #
19
+ # @see https://flowbite.com/docs/components/sidebar/
20
+ # @lookbook_embed SidebarPreview
21
+ class Sidebar < ViewComponent::Base
22
+ class << self
23
+ def classes
24
+ [
25
+ "fixed", "top-0", "left-0", "z-40", "w-64", "h-screen",
26
+ "transition-transform", "-translate-x-full", "sm:translate-x-0"
27
+ ]
28
+ end
29
+ end
30
+
31
+ # @param class [Array<String>] Additional CSS classes for the sidebar
32
+ # container.
33
+ # @param options [Hash] Additional HTML options for the sidebar container.
34
+ def initialize(class: nil, **options)
35
+ super()
36
+ @class = Array.wrap(binding.local_variable_get(:class))
37
+ @options = options
38
+ end
39
+
40
+ def call
41
+ content_tag(:aside, aside_options) do
42
+ content_tag(:div, class: wrapper_classes) do
43
+ content
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def aside_classes
51
+ self.class.classes + @class
52
+ end
53
+
54
+ def aside_options
55
+ {class: aside_classes, "aria-label": "Sidebar"}.merge(@options)
56
+ end
57
+
58
+ def wrapper_classes
59
+ ["h-full", "px-3", "py-4", "overflow-y-auto", "bg-neutral-primary-soft"]
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,20 @@
1
+ <%= tag.div(role: "status", **options) do %>
2
+ <svg
3
+ aria-hidden="true"
4
+ class="<%= classes.join(" ") %>"
5
+ fill="none"
6
+ viewBox="0 0 100 101"
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ >
9
+ <path
10
+ d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
11
+ fill="currentColor"
12
+ />
13
+
14
+ <path
15
+ d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
16
+ />
17
+ </svg>
18
+
19
+ <span class="sr-only"><%= message %></span>
20
+ <% end %>
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ # Renders a loading spinner.
5
+ #
6
+ # @example Basic usage
7
+ # <%= render(Flowbite::Spinner.new) %>
8
+ #
9
+ # @example With a custom style and size
10
+ # <%= render(Flowbite::Spinner.new(style: :success, size: :lg)) %>
11
+ #
12
+ # @see https://flowbite.com/docs/components/spinner/
13
+ # @lookbook_embed SpinnerPreview
14
+ class Spinner < ViewComponent::Base
15
+ SIZES = {
16
+ xs: ["w-4", "h-4"],
17
+ sm: ["w-6", "h-6"],
18
+ default: ["w-8", "h-8"],
19
+ lg: ["w-10", "h-10"],
20
+ xl: ["w-12", "h-12"]
21
+ }.freeze
22
+
23
+ class << self
24
+ def classes(size: :default, style: :brand)
25
+ style = styles.fetch(style)
26
+ ["text-neutral-tertiary", "animate-spin"] + style.fetch(:default) + sizes.fetch(size)
27
+ end
28
+
29
+ def sizes
30
+ SIZES
31
+ end
32
+
33
+ def styles
34
+ Flowbite::Styles.from_hash(
35
+ {
36
+ brand: {default: ["fill-brand"]},
37
+ danger: {default: ["fill-danger"]},
38
+ success: {default: ["fill-success"]},
39
+ warning: {default: ["fill-warning"]}
40
+ }.freeze
41
+ )
42
+ end
43
+ end
44
+
45
+ attr_reader :message, :options
46
+
47
+ # @param class [String, Array<String>] Additional CSS classes for the spinner icon.
48
+ # @param message [String] Text for screen readers announcing the loading state.
49
+ # @param options [Hash] Additional HTML options for the container element.
50
+ # @param size [Symbol] The size of the spinner (:xs, :sm, :default, :lg, :xl).
51
+ # @param style [Symbol] The color style of the spinner (:brand, :danger, :success, :warning).
52
+ def initialize(class: nil, message: "Loading...", size: :default, style: :brand, **options)
53
+ @class = Array.wrap(binding.local_variable_get(:class))
54
+ @message = message
55
+ @size = size
56
+ @style = style
57
+ @options = options
58
+ end
59
+
60
+ private
61
+
62
+ def classes
63
+ self.class.classes(size: @size, style: @style) + @class
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,17 @@
1
+ <%= tag.button(**button_options) do %>
2
+ <svg
3
+ aria-hidden="true"
4
+ class="w-3 h-3"
5
+ fill="none"
6
+ viewBox="0 0 14 14"
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ >
9
+ <path
10
+ d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
11
+ stroke="currentColor"
12
+ stroke-linecap="round"
13
+ stroke-linejoin="round"
14
+ stroke-width="2"
15
+ />
16
+ </svg>
17
+ <% end %>
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ class Toast
5
+ # Renders the close button of a toast notification.
6
+ #
7
+ # @param options [Hash] Additional HTML options for the button element.
8
+ # These are merged over the default options.
9
+ class CloseButton < ViewComponent::Base
10
+ class << self
11
+ # Styles from https://flowbite.com/docs/components/toast/#default-toast
12
+ # rubocop:disable Layout/LineLength
13
+ def classes
14
+ ["ms-auto", "flex", "items-center", "justify-center", "text-body", "hover:text-heading", "bg-transparent", "box-border", "border", "border-transparent", "hover:bg-neutral-secondary-medium", "focus:ring-4", "focus:ring-neutral-tertiary", "font-medium", "leading-5", "rounded", "text-sm", "h-8", "w-8", "focus:outline-none"]
15
+ end
16
+ # rubocop:enable Layout/LineLength
17
+ end
18
+
19
+ def initialize(**options)
20
+ @options = options || {}
21
+ end
22
+
23
+ def button_options
24
+ {
25
+ aria: {label: "Close"},
26
+ class: self.class.classes,
27
+ type: "button"
28
+ }.merge(@options)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,5 +1,11 @@
1
1
  <div class="<%= container_classes.join(" ") %>">
2
- <svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
3
- <path d="<%= svg_path %>"/>
2
+ <svg
3
+ aria-hidden="true"
4
+ class="w-4 h-4"
5
+ fill="currentColor"
6
+ viewBox="0 0 20 20"
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ >
9
+ <path d="<%= svg_path %>" />
4
10
  </svg>
5
11
  </div>