flowbite-components 0.3.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -2
  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 +4 -1
  13. data/app/components/flowbite/card.rb +4 -8
  14. data/app/components/flowbite/input_field/input_field.html.erb +1 -0
  15. data/app/components/flowbite/pagination/chevron_left_icon.rb +34 -0
  16. data/app/components/flowbite/pagination/chevron_right_icon.rb +34 -0
  17. data/app/components/flowbite/pagination/gap.rb +24 -0
  18. data/app/components/flowbite/pagination/link.rb +52 -0
  19. data/app/components/flowbite/pagination/next_link.rb +67 -0
  20. data/app/components/flowbite/pagination/pagination.html.erb +11 -0
  21. data/app/components/flowbite/pagination/previous_link.rb +67 -0
  22. data/app/components/flowbite/pagination.rb +63 -0
  23. data/app/components/flowbite/progress/progress.html.erb +14 -0
  24. data/app/components/flowbite/progress.rb +163 -0
  25. data/app/components/flowbite/rating/rating.html.erb +13 -0
  26. data/app/components/flowbite/rating/star.html.erb +13 -0
  27. data/app/components/flowbite/rating/star.rb +59 -0
  28. data/app/components/flowbite/rating.rb +64 -0
  29. data/app/components/flowbite/spinner/spinner.html.erb +20 -0
  30. data/app/components/flowbite/spinner.rb +66 -0
  31. data/app/components/flowbite/toast/close_button.html.erb +17 -0
  32. data/app/components/flowbite/toast/close_button.rb +32 -0
  33. data/app/components/flowbite/toast/icon.html.erb +8 -2
  34. data/app/components/flowbite/toast/toast.html.erb +1 -28
  35. data/app/components/flowbite/toast.rb +39 -1
  36. data/lib/flowbite/components/version.rb +1 -1
  37. metadata +32 -3
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ class Pagination
5
+ # Renders the chevron-left icon shown on a pagination's previous link.
6
+ #
7
+ # This is automatically used by {Flowbite::Pagination::PreviousLink}, but
8
+ # can be used standalone if needed.
9
+ #
10
+ # @example Standalone usage
11
+ # <%= render Flowbite::Pagination::ChevronLeftIcon.new %>
12
+ class ChevronLeftIcon < ViewComponent::Base
13
+ def call
14
+ tag.svg(
15
+ class: "w-4 h-4 rtl:rotate-180",
16
+ "aria-hidden": "true",
17
+ xmlns: "http://www.w3.org/2000/svg",
18
+ width: 24,
19
+ height: 24,
20
+ fill: "none",
21
+ viewBox: "0 0 24 24"
22
+ ) do
23
+ tag.path(
24
+ stroke: "currentColor",
25
+ "stroke-linecap": "round",
26
+ "stroke-linejoin": "round",
27
+ "stroke-width": "2",
28
+ d: "m15 19-7-7 7-7"
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ class Pagination
5
+ # Renders the chevron-right icon shown on a pagination's next link.
6
+ #
7
+ # This is automatically used by {Flowbite::Pagination::NextLink}, but can
8
+ # be used standalone if needed.
9
+ #
10
+ # @example Standalone usage
11
+ # <%= render Flowbite::Pagination::ChevronRightIcon.new %>
12
+ class ChevronRightIcon < ViewComponent::Base
13
+ def call
14
+ tag.svg(
15
+ class: "w-4 h-4 rtl:rotate-180",
16
+ "aria-hidden": "true",
17
+ xmlns: "http://www.w3.org/2000/svg",
18
+ width: 24,
19
+ height: 24,
20
+ fill: "none",
21
+ viewBox: "0 0 24 24"
22
+ ) do
23
+ tag.path(
24
+ stroke: "currentColor",
25
+ "stroke-linecap": "round",
26
+ "stroke-linejoin": "round",
27
+ "stroke-width": "2",
28
+ d: "m9 5 7 7-7 7"
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ class Pagination
5
+ # Renders a gap indicator between page numbers in a pagination component.
6
+ #
7
+ # This is automatically used by {Flowbite::Pagination}, but can be used
8
+ # standalone if needed.
9
+ #
10
+ # @example Standalone usage
11
+ # <%= render Flowbite::Pagination::Gap.new %>
12
+ class Gap < ViewComponent::Base
13
+ class << self
14
+ def classes
15
+ ["flex", "items-center", "justify-center", "text-body", "bg-neutral-secondary-medium", "box-border", "border", "border-default-medium", "hover:bg-neutral-tertiary-medium", "hover:text-heading", "font-medium", "text-sm", "w-9", "h-9", "focus:outline-none"]
16
+ end
17
+ end
18
+
19
+ def call
20
+ content_tag(:li, "…", class: self.class.classes)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ class Pagination
5
+ # Renders a single page number link in a pagination component.
6
+ #
7
+ # This is automatically used by {Flowbite::Pagination}, but can be used
8
+ # standalone if needed.
9
+ #
10
+ # @param current [Boolean] Whether this link represents the current page.
11
+ # @param page [Integer] The page number to display.
12
+ # @param url [String] The URL for the page.
13
+ #
14
+ # @example Standalone usage
15
+ # <%= render Flowbite::Pagination::Link.new(page: 2, url: "/posts?page=2") %>
16
+ class Link < ViewComponent::Base
17
+ class << self
18
+ def classes(current: false)
19
+ if current
20
+ ["flex", "items-center", "justify-center", "text-fg-brand", "bg-neutral-tertiary-medium", "box-border", "border", "border-default-medium", "hover:text-fg-brand", "font-medium", "text-sm", "w-9", "h-9", "focus:outline-none"]
21
+ else
22
+ ["flex", "items-center", "justify-center", "text-body", "bg-neutral-secondary-medium", "box-border", "border", "border-default-medium", "hover:bg-neutral-tertiary-medium", "hover:text-heading", "font-medium", "text-sm", "w-9", "h-9", "focus:outline-none"]
23
+ end
24
+ end
25
+ end
26
+
27
+ attr_reader :page, :url
28
+
29
+ def initialize(page:, url:, current: false)
30
+ @current = current
31
+ @page = page
32
+ @url = url
33
+ end
34
+
35
+ def call
36
+ content_tag(:li) { render_link }
37
+ end
38
+
39
+ def current?
40
+ !!@current
41
+ end
42
+
43
+ private
44
+
45
+ def render_link
46
+ link_options = {class: self.class.classes(current: current?)}
47
+ link_options[:aria] = {current: "page"} if current?
48
+ content_tag(:a, page, href: url, **link_options)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ class Pagination
5
+ # Renders the "Next page" link in a pagination component.
6
+ #
7
+ # This is automatically used by {Flowbite::Pagination}, but can be used
8
+ # standalone if needed.
9
+ #
10
+ # @param disabled [Boolean] Whether to disable the link. When disabled, a
11
+ # non-interactive +<span>+ is rendered instead of a link, so the control
12
+ # can't be focused or activated.
13
+ # @param url [String] The URL for the next page. Ignored if +disabled+.
14
+ #
15
+ # @example Standalone usage
16
+ # <%= render Flowbite::Pagination::NextLink.new(url: "/posts?page=2") %>
17
+ class NextLink < ViewComponent::Base
18
+ class << self
19
+ def classes(disabled: false)
20
+ classes = ["flex", "items-center", "justify-center", "text-body", "bg-neutral-secondary-medium", "box-border", "border", "border-default-medium", "hover:bg-neutral-tertiary-medium", "hover:text-heading", "font-medium", "rounded-e-base", "text-sm", "w-9", "h-9", "focus:outline-none"]
21
+ classes += ["opacity-50", "cursor-not-allowed"] if disabled
22
+ classes
23
+ end
24
+ end
25
+
26
+ attr_reader :url
27
+
28
+ def initialize(url:, disabled: false)
29
+ @disabled = disabled
30
+ @url = url
31
+ end
32
+
33
+ def call
34
+ content_tag(:li) { render_link }
35
+ end
36
+
37
+ def disabled?
38
+ !!@disabled
39
+ end
40
+
41
+ private
42
+
43
+ def render_link
44
+ content_tag(tag_name, tag_options) do
45
+ concat(content_tag(:span, "Next", class: "sr-only"))
46
+ concat(render(Flowbite::Pagination::ChevronRightIcon.new))
47
+ end
48
+ end
49
+
50
+ def tag_name
51
+ disabled? ? :span : :a
52
+ end
53
+
54
+ def tag_options
55
+ options = {class: self.class.classes(disabled: disabled?)}
56
+
57
+ if disabled?
58
+ options[:"aria-disabled"] = "true"
59
+ else
60
+ options[:href] = url
61
+ end
62
+
63
+ options
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,11 @@
1
+ <%= tag.nav("aria-label": "Pagination", **options) do %>
2
+ <ul class="<%= list_classes.join(" ") %>">
3
+ <%= render(Flowbite::Pagination::PreviousLink.new(url: previous_url, disabled: previous_url.nil?)) %>
4
+
5
+ <% pages.each do |page| %>
6
+ <%= page %>
7
+ <% end %>
8
+
9
+ <%= render(Flowbite::Pagination::NextLink.new(url: next_url, disabled: next_url.nil?)) %>
10
+ </ul>
11
+ <% end %>
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ class Pagination
5
+ # Renders the "Previous page" link in a pagination component.
6
+ #
7
+ # This is automatically used by {Flowbite::Pagination}, but can be used
8
+ # standalone if needed.
9
+ #
10
+ # @param disabled [Boolean] Whether to disable the link. When disabled, a
11
+ # non-interactive +<span>+ is rendered instead of a link, so the control
12
+ # can't be focused or activated.
13
+ # @param url [String] The URL for the previous page. Ignored if +disabled+.
14
+ #
15
+ # @example Standalone usage
16
+ # <%= render Flowbite::Pagination::PreviousLink.new(url: "/posts?page=1") %>
17
+ class PreviousLink < ViewComponent::Base
18
+ class << self
19
+ def classes(disabled: false)
20
+ classes = ["flex", "items-center", "justify-center", "text-body", "bg-neutral-secondary-medium", "box-border", "border", "border-default-medium", "hover:bg-neutral-tertiary-medium", "hover:text-heading", "font-medium", "rounded-s-base", "text-sm", "w-9", "h-9", "focus:outline-none"]
21
+ classes += ["opacity-50", "cursor-not-allowed"] if disabled
22
+ classes
23
+ end
24
+ end
25
+
26
+ attr_reader :url
27
+
28
+ def initialize(url:, disabled: false)
29
+ @disabled = disabled
30
+ @url = url
31
+ end
32
+
33
+ def call
34
+ content_tag(:li) { render_link }
35
+ end
36
+
37
+ def disabled?
38
+ !!@disabled
39
+ end
40
+
41
+ private
42
+
43
+ def render_link
44
+ content_tag(tag_name, tag_options) do
45
+ concat(content_tag(:span, "Previous", class: "sr-only"))
46
+ concat(render(Flowbite::Pagination::ChevronLeftIcon.new))
47
+ end
48
+ end
49
+
50
+ def tag_name
51
+ disabled? ? :span : :a
52
+ end
53
+
54
+ def tag_options
55
+ options = {class: self.class.classes(disabled: disabled?)}
56
+
57
+ if disabled?
58
+ options[:"aria-disabled"] = "true"
59
+ else
60
+ options[:href] = url
61
+ end
62
+
63
+ options
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flowbite
4
+ # Renders pagination controls for navigating between pages of results.
5
+ #
6
+ # This component doesn't know about pages, page numbers, or any pagination
7
+ # library. The caller decides which pages to show (including where to
8
+ # place gaps) using the +pages+ slot, and provides the URLs for the
9
+ # previous/next links directly.
10
+ #
11
+ # @example Usage
12
+ # <%= render(Flowbite::Pagination.new(
13
+ # previous_url: (posts_path(page: @pagy.page - 1) if @pagy.page > 1),
14
+ # next_url: (posts_path(page: @pagy.page + 1) if @pagy.page < @pagy.pages)
15
+ # )) do |pagination| %>
16
+ # <% (1..@pagy.pages).each do |page| %>
17
+ # <% pagination.with_page do %>
18
+ # <%= render(Flowbite::Pagination::Link.new(
19
+ # page: page,
20
+ # url: posts_path(page: page),
21
+ # current: page == @pagy.page
22
+ # )) %>
23
+ # <% end %>
24
+ # <% end %>
25
+ # <% end %>
26
+ #
27
+ # @viewcomponent_slot [Flowbite::Pagination::Link, Flowbite::Pagination::Gap]
28
+ # pages The pages to show. Use {Flowbite::Pagination::Link} for page
29
+ # numbers, and {Flowbite::Pagination::Gap} where numbers are skipped.
30
+ #
31
+ # @see https://flowbite.com/docs/components/pagination/
32
+ # @lookbook_embed PaginationPreview
33
+ class Pagination < ViewComponent::Base
34
+ renders_many :pages
35
+
36
+ class << self
37
+ def classes
38
+ ["flex", "-space-x-px", "text-sm"]
39
+ end
40
+ end
41
+
42
+ attr_reader :next_url, :options, :previous_url
43
+
44
+ # @param class [Array<String>] Additional CSS classes for the list element.
45
+ # @param next_url [String] The URL for the next page. Omit to disable the
46
+ # next link, e.g. when already on the last page.
47
+ # @param options [Hash] Additional HTML options for the nav element.
48
+ # @param previous_url [String] The URL for the previous page. Omit to
49
+ # disable the previous link, e.g. when already on the first page.
50
+ def initialize(next_url: nil, previous_url: nil, class: nil, **options)
51
+ @next_url = next_url
52
+ @previous_url = previous_url
53
+ @class = Array.wrap(binding.local_variable_get(:class))
54
+ @options = options
55
+ end
56
+
57
+ private
58
+
59
+ def list_classes
60
+ self.class.classes + @class
61
+ end
62
+ end
63
+ end
@@ -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