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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -2
- data/README.md +3 -5
- data/app/components/flowbite/alert/alert.html.erb +36 -0
- data/app/components/flowbite/alert/icon.rb +53 -0
- data/app/components/flowbite/alert.rb +156 -0
- data/app/components/flowbite/avatar/avatar.html.erb +23 -0
- data/app/components/flowbite/avatar/dot.rb +85 -0
- data/app/components/flowbite/avatar/placeholder_icon.html.erb +13 -0
- data/app/components/flowbite/avatar/placeholder_icon.rb +44 -0
- data/app/components/flowbite/avatar.rb +132 -0
- data/app/components/flowbite/badge/badge.html.erb +4 -1
- data/app/components/flowbite/card.rb +4 -8
- data/app/components/flowbite/input_field/input_field.html.erb +1 -0
- data/app/components/flowbite/pagination/chevron_left_icon.rb +34 -0
- data/app/components/flowbite/pagination/chevron_right_icon.rb +34 -0
- data/app/components/flowbite/pagination/gap.rb +24 -0
- data/app/components/flowbite/pagination/link.rb +52 -0
- data/app/components/flowbite/pagination/next_link.rb +67 -0
- data/app/components/flowbite/pagination/pagination.html.erb +11 -0
- data/app/components/flowbite/pagination/previous_link.rb +67 -0
- data/app/components/flowbite/pagination.rb +63 -0
- data/app/components/flowbite/progress/progress.html.erb +14 -0
- data/app/components/flowbite/progress.rb +163 -0
- data/app/components/flowbite/rating/rating.html.erb +13 -0
- data/app/components/flowbite/rating/star.html.erb +13 -0
- data/app/components/flowbite/rating/star.rb +59 -0
- data/app/components/flowbite/rating.rb +64 -0
- data/app/components/flowbite/spinner/spinner.html.erb +20 -0
- data/app/components/flowbite/spinner.rb +66 -0
- data/app/components/flowbite/toast/close_button.html.erb +17 -0
- data/app/components/flowbite/toast/close_button.rb +32 -0
- data/app/components/flowbite/toast/icon.html.erb +8 -2
- data/app/components/flowbite/toast/toast.html.erb +1 -28
- data/app/components/flowbite/toast.rb +39 -1
- data/lib/flowbite/components/version.rb +1 -1
- 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
|