panda-core 0.2.4 → 0.6.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/tailwind/application.css +199 -7
  3. data/app/assets/tailwind/tailwind.config.js +8 -0
  4. data/app/components/panda/core/UI/badge.rb +107 -0
  5. data/app/components/panda/core/UI/button.rb +110 -0
  6. data/app/components/panda/core/UI/card.rb +88 -0
  7. data/app/components/panda/core/admin/breadcrumb_component.rb +133 -0
  8. data/app/components/panda/core/admin/button_component.rb +46 -28
  9. data/app/components/panda/core/admin/container_component.rb +75 -4
  10. data/app/components/panda/core/admin/file_gallery_component.rb +157 -0
  11. data/app/components/panda/core/admin/flash_message_component.rb +98 -15
  12. data/app/components/panda/core/admin/form_error_component.rb +48 -0
  13. data/app/components/panda/core/admin/form_input_component.rb +50 -0
  14. data/app/components/panda/core/admin/form_select_component.rb +68 -0
  15. data/app/components/panda/core/admin/heading_component.rb +53 -24
  16. data/app/components/panda/core/admin/page_header_component.rb +107 -0
  17. data/app/components/panda/core/admin/panel_component.rb +33 -4
  18. data/app/components/panda/core/admin/slideover_component.rb +66 -4
  19. data/app/components/panda/core/admin/statistics_component.rb +19 -0
  20. data/app/components/panda/core/admin/tab_bar_component.rb +101 -0
  21. data/app/components/panda/core/admin/table_component.rb +92 -11
  22. data/app/components/panda/core/admin/tag_component.rb +58 -16
  23. data/app/components/panda/core/admin/user_activity_component.rb +43 -0
  24. data/app/components/panda/core/admin/user_display_component.rb +77 -0
  25. data/app/components/panda/core/base.rb +122 -0
  26. data/app/controllers/panda/core/admin/base_controller.rb +68 -0
  27. data/app/controllers/panda/core/admin/dashboard_controller.rb +5 -3
  28. data/app/controllers/panda/core/admin/my_profile_controller.rb +4 -4
  29. data/app/controllers/panda/core/admin/sessions_controller.rb +15 -8
  30. data/app/controllers/panda/core/admin/test_sessions_controller.rb +60 -0
  31. data/app/helpers/panda/core/asset_helper.rb +31 -5
  32. data/app/helpers/panda/core/sessions_helper.rb +27 -2
  33. data/app/javascript/panda/core/application.js +8 -1
  34. data/app/javascript/panda/core/controllers/alert_controller.js +38 -0
  35. data/app/javascript/panda/core/controllers/index.js +3 -3
  36. data/app/javascript/panda/core/controllers/toggle_controller.js +41 -0
  37. data/app/javascript/panda/core/tailwindplus-elements.js +31 -0
  38. data/app/javascript/panda/core/vendor/@hotwired--stimulus.js +4 -0
  39. data/app/javascript/panda/core/vendor/@hotwired--turbo.js +160 -0
  40. data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +4 -0
  41. data/app/models/panda/core/user.rb +61 -14
  42. data/app/services/panda/core/attach_avatar_service.rb +67 -0
  43. data/app/views/layouts/panda/core/admin.html.erb +40 -3
  44. data/app/views/layouts/panda/core/admin_simple.html.erb +6 -0
  45. data/app/views/panda/core/admin/dashboard/_default_content.html.erb +4 -4
  46. data/app/views/panda/core/admin/dashboard/show.html.erb +2 -2
  47. data/app/views/panda/core/admin/my_profile/edit.html.erb +36 -25
  48. data/app/views/panda/core/admin/sessions/new.html.erb +9 -10
  49. data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +27 -34
  50. data/app/views/panda/core/admin/shared/_flash.html.erb +4 -30
  51. data/app/views/panda/core/admin/shared/_sidebar.html.erb +41 -20
  52. data/app/views/panda/core/shared/_header.html.erb +13 -5
  53. data/config/importmap.rb +19 -6
  54. data/config/routes.rb +10 -3
  55. data/db/migrate/20250810120000_add_current_theme_to_panda_core_users.rb +7 -0
  56. data/db/migrate/20250811120000_add_oauth_avatar_url_to_panda_core_users.rb +7 -0
  57. data/lib/panda/core/asset_loader.rb +23 -8
  58. data/lib/panda/core/configuration.rb +12 -9
  59. data/lib/panda/core/debug.rb +47 -0
  60. data/lib/panda/core/engine.rb +55 -9
  61. data/lib/panda/core/services/base_service.rb +19 -4
  62. data/lib/panda/core/version.rb +1 -1
  63. data/lib/panda/core.rb +2 -0
  64. data/lib/tasks/panda_core_users.rake +158 -0
  65. metadata +103 -14
  66. data/app/components/panda/core/admin/container_component.html.erb +0 -12
  67. data/app/components/panda/core/admin/flash_message_component.html.erb +0 -31
  68. data/app/components/panda/core/admin/panel_component.html.erb +0 -7
  69. data/app/components/panda/core/admin/slideover_component.html.erb +0 -9
  70. data/app/components/panda/core/admin/table_component.html.erb +0 -29
  71. data/app/controllers/panda/core/admin_controller.rb +0 -30
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class FormInputComponent < Panda::Core::Base
7
+ prop :name, String
8
+ prop :value, _Nilable(String), default: -> {}
9
+ prop :type, Symbol, default: :text
10
+ prop :placeholder, _Nilable(String), default: -> {}
11
+ prop :required, _Boolean, default: -> { false }
12
+ prop :disabled, _Boolean, default: -> { false }
13
+ prop :autocomplete, _Nilable(String), default: -> {}
14
+
15
+ def view_template
16
+ input(**@attrs)
17
+ end
18
+
19
+ def default_attrs
20
+ base_attrs = {
21
+ type: @type.to_s,
22
+ name: @name,
23
+ id: @name.to_s.gsub(/[\[\]]/, "_").gsub("__", "_").chomp("_"),
24
+ class: input_classes
25
+ }
26
+
27
+ base_attrs[:value] = @value if @value
28
+ base_attrs[:placeholder] = @placeholder if @placeholder
29
+ base_attrs[:required] = true if @required
30
+ base_attrs[:disabled] = true if @disabled
31
+ base_attrs[:autocomplete] = @autocomplete if @autocomplete
32
+
33
+ base_attrs
34
+ end
35
+
36
+ private
37
+
38
+ def input_classes
39
+ classes = "block w-full rounded-md border-0 p-2 text-gray-900 ring-1 ring-inset placeholder:text-gray-300 focus:ring-1 focus:ring-inset sm:leading-6"
40
+
41
+ if @disabled
42
+ classes + " ring-gray-300 focus:ring-gray-300 bg-gray-50 cursor-not-allowed"
43
+ else
44
+ classes + " ring-mid focus:ring-dark hover:cursor-pointer"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class FormSelectComponent < Panda::Core::Base
7
+ prop :name, String
8
+ prop :options, Array
9
+ prop :selected, _Nilable(_Union(String, Integer)), default: -> {}
10
+ prop :prompt, _Nilable(String), default: -> {}
11
+ prop :required, _Boolean, default: -> { false }
12
+ prop :disabled, _Boolean, default: -> { false }
13
+ prop :include_blank, _Boolean, default: -> { false }
14
+
15
+ def view_template
16
+ select(**@attrs) do
17
+ render_prompt if @prompt
18
+ render_blank if @include_blank && !@prompt
19
+ render_options
20
+ end
21
+ end
22
+
23
+ def default_attrs
24
+ base_attrs = {
25
+ name: @name,
26
+ id: @name.to_s.gsub(/[\[\]]/, "_").gsub("__", "_").chomp("_"),
27
+ class: select_classes
28
+ }
29
+
30
+ base_attrs[:required] = true if @required
31
+ base_attrs[:disabled] = true if @disabled
32
+
33
+ base_attrs
34
+ end
35
+
36
+ private
37
+
38
+ def select_classes
39
+ classes = "block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset focus:ring-1 focus:ring-inset sm:leading-6"
40
+
41
+ if @disabled
42
+ classes + " ring-gray-300 focus:ring-gray-300 bg-gray-50 cursor-not-allowed"
43
+ else
44
+ classes + " ring-mid focus:ring-dark hover:cursor-pointer"
45
+ end
46
+ end
47
+
48
+ def render_prompt
49
+ option(value: "", disabled: true, selected: @selected.nil?) { @prompt }
50
+ end
51
+
52
+ def render_blank
53
+ option(value: "") { "" }
54
+ end
55
+
56
+ def render_options
57
+ @options.each do |option_data|
58
+ label, value = option_data
59
+ option_attrs = {value: value.to_s}
60
+ option_attrs[:selected] = true if value.to_s == @selected.to_s
61
+
62
+ option(**option_attrs) { label.to_s }
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -3,42 +3,71 @@
3
3
  module Panda
4
4
  module Core
5
5
  module Admin
6
- class HeadingComponent < ViewComponent::Base
7
- renders_many :buttons, Panda::Core::Admin::ButtonComponent
6
+ class HeadingComponent < Panda::Core::Base
7
+ prop :text, String
8
+ prop :level, _Nilable(_Union(Integer, Symbol)), default: -> { 2 }
9
+ prop :icon, String, default: ""
10
+ prop :additional_styles, _Nilable(_Union(String, Array)), default: -> { "" }
8
11
 
9
- attr_reader :text, :level, :icon, :additional_styles
12
+ def view_template(&block)
13
+ # Capture any buttons defined via block
14
+ instance_eval(&block) if block_given?
10
15
 
11
- def initialize(text:, level: 2, icon: "", additional_styles: "")
12
- @text = text
13
- @level = level
14
- @icon = icon
15
- @additional_styles = additional_styles
16
- @additional_styles = @additional_styles.split(" ") if @additional_styles.is_a?(String)
16
+ case @level
17
+ when 1
18
+ h1(class: heading_classes) { render_content }
19
+ when 2
20
+ h2(class: heading_classes) { render_content }
21
+ when 3
22
+ h3(class: heading_classes) { render_content }
23
+ when :panel
24
+ h3(class: panel_heading_classes) { @text }
25
+ else
26
+ h2(class: heading_classes) { render_content }
27
+ end
28
+ end
29
+
30
+ def button(**props)
31
+ @buttons ||= []
32
+ @buttons << Panda::Core::Admin::ButtonComponent.new(**props)
17
33
  end
18
34
 
19
- def call
20
- output = ""
21
- output += content_tag(:div, @text, class: "grow")
35
+ private
22
36
 
23
- if buttons?
24
- output += content_tag(:span, class: "actions flex gap-x-2 -mt-1") do
25
- safe_join(buttons, "")
26
- end
37
+ def render_content
38
+ div(class: "grow flex items-center gap-x-2") do
39
+ i(class: @icon) if @icon.present?
40
+ span { @text }
27
41
  end
28
42
 
29
- output = output.html_safe
30
- base_heading_styles = "flex pt-1 text-black mb-5 -mt-1"
43
+ span(class: "actions flex gap-x-2 -mt-1 min-h-[2.5rem]") do
44
+ @buttons&.each { |btn| render(btn) }
45
+ end
46
+ end
31
47
 
32
- case level
48
+ def heading_classes
49
+ base = "flex pt-1 text-black mb-5 -mt-1"
50
+ styles = case @level
33
51
  when 1
34
- content_tag(:h1, output, class: [base_heading_styles, "text-2xl font-medium", @additional_styles])
52
+ "text-2xl font-medium"
35
53
  when 2
36
- content_tag(:h2, output, class: [base_heading_styles, "text-xl font-medium", @additional_styles])
54
+ "text-xl font-medium"
37
55
  when 3
38
- content_tag(:h3, output, class: [base_heading_styles, "text-xl", "font-light", @additional_styles])
39
- when :panel
40
- content_tag(:h3, output, class: ["text-base font-medium p-4 text-white"])
56
+ "text-xl font-light"
57
+ else
58
+ "text-xl font-medium"
41
59
  end
60
+
61
+ [base, styles, *additional_styles_array].compact.join(" ")
62
+ end
63
+
64
+ def panel_heading_classes
65
+ "text-base font-medium px-4 py-3 text-white"
66
+ end
67
+
68
+ def additional_styles_array
69
+ return [] if @additional_styles.blank?
70
+ @additional_styles.is_a?(String) ? @additional_styles.split(" ") : @additional_styles
42
71
  end
43
72
  end
44
73
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ # Page header component with title, optional breadcrumbs, and action buttons.
7
+ #
8
+ # Follows Tailwind UI Plus pattern for page headers with responsive layout
9
+ # and support for multiple action buttons.
10
+ #
11
+ # @example Basic header with title only
12
+ # render Panda::Core::Admin::PageHeaderComponent.new(
13
+ # title: "Back End Developer"
14
+ # )
15
+ #
16
+ # @example Header with breadcrumbs
17
+ # render Panda::Core::Admin::PageHeaderComponent.new(
18
+ # title: "Back End Developer",
19
+ # breadcrumbs: [
20
+ # { text: "Jobs", href: "/admin/jobs" },
21
+ # { text: "Engineering", href: "/admin/jobs/engineering" },
22
+ # { text: "Back End Developer", href: "/admin/jobs/engineering/1" }
23
+ # ]
24
+ # )
25
+ #
26
+ # @example Header with action buttons using block
27
+ # render Panda::Core::Admin::PageHeaderComponent.new(
28
+ # title: "Back End Developer",
29
+ # breadcrumbs: breadcrumb_items
30
+ # ) do |header|
31
+ # header.button(text: "Edit", variant: :secondary, href: edit_path)
32
+ # header.button(text: "Publish", variant: :primary, href: publish_path)
33
+ # end
34
+ #
35
+ class PageHeaderComponent < Panda::Core::Base
36
+ prop :title, String
37
+ prop :breadcrumbs, _Nilable(Array), default: -> {}
38
+ prop :show_back, _Boolean, default: true
39
+
40
+ def initialize(**props)
41
+ super
42
+ @buttons = []
43
+ end
44
+
45
+ def view_template(&block)
46
+ # Allow buttons to be defined via block
47
+ instance_eval(&block) if block_given?
48
+
49
+ div do
50
+ # Breadcrumbs section
51
+ if @breadcrumbs
52
+ render Panda::Core::Admin::BreadcrumbComponent.new(
53
+ items: @breadcrumbs,
54
+ show_back: @show_back
55
+ )
56
+ end
57
+
58
+ # Title and actions section
59
+ div(class: "mt-2 md:flex md:items-center md:justify-between") do
60
+ # Title
61
+ div(class: "min-w-0 flex-1") do
62
+ h2(class: "text-2xl/7 font-bold text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight dark:text-white") do
63
+ @title
64
+ end
65
+ end
66
+
67
+ # Action buttons
68
+ if @buttons.any?
69
+ div(class: "mt-4 flex shrink-0 md:mt-0 md:ml-4") do
70
+ @buttons.each_with_index do |button_data, index|
71
+ render create_button(button_data, index)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ # Define a button to be rendered in the header actions area
80
+ #
81
+ # @param text [String] Button text
82
+ # @param variant [Symbol] Button variant (:primary or :secondary)
83
+ # @param href [String] Link href
84
+ # @param props [Hash] Additional button properties
85
+ def button(text:, variant: :secondary, href: "#", **props)
86
+ @buttons << {text: text, variant: variant, href: href, **props}
87
+ end
88
+
89
+ private
90
+
91
+ def create_button(button_data, index)
92
+ Panda::Core::UI::Button.new(
93
+ text: button_data[:text],
94
+ variant: button_data[:variant],
95
+ href: button_data[:href],
96
+ class: button_margin_class(index),
97
+ **button_data.except(:text, :variant, :href)
98
+ )
99
+ end
100
+
101
+ def button_margin_class(index)
102
+ index.zero? ? "" : "ml-3"
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -3,10 +3,39 @@
3
3
  module Panda
4
4
  module Core
5
5
  module Admin
6
- class PanelComponent < ViewComponent::Base
7
- renders_one :heading, lambda { |text:, icon: "", level: :panel, additional_styles: ""|
8
- Panda::Core::Admin::HeadingComponent.new(text: text, icon: icon, level: level, additional_styles: additional_styles)
9
- }
6
+ class PanelComponent < Panda::Core::Base
7
+ def view_template(&block)
8
+ # Capture block content differently based on context (ERB vs Phlex)
9
+ if block_given?
10
+ if defined?(view_context) && view_context
11
+ # Called from ERB - capture HTML output
12
+ @body_html = view_context.capture { yield(self) }
13
+ else
14
+ # Called from Phlex - execute block directly to set instance variables
15
+ yield(self)
16
+ end
17
+ end
18
+
19
+ div(class: "col-span-3 mt-5 rounded-lg shadow-md bg-dark shadow-inherit/20") do
20
+ @heading_content&.call
21
+
22
+ div(class: "p-4 text-black bg-white rounded-b-lg") do
23
+ if @body_content
24
+ @body_content.call
25
+ elsif @body_html
26
+ raw(@body_html)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def heading(**props)
33
+ @heading_content = -> { render(Panda::Core::Admin::HeadingComponent.new(**props.merge(level: :panel))) }
34
+ end
35
+
36
+ def body(&block)
37
+ @body_content = block
38
+ end
10
39
  end
11
40
  end
12
41
  end
@@ -3,11 +3,73 @@
3
3
  module Panda
4
4
  module Core
5
5
  module Admin
6
- class SlideoverComponent < ViewComponent::Base
7
- attr_reader :title
6
+ class SlideoverComponent < Panda::Core::Base
7
+ prop :title, String, default: "Settings"
8
+ prop :open, _Nilable(_Boolean), default: -> { false }
8
9
 
9
- def initialize(title: "Settings")
10
- @title = title
10
+ def view_template(&block)
11
+ # Capture block content
12
+ if block_given?
13
+ if defined?(view_context) && view_context
14
+ @content_html = view_context.capture(&block)
15
+ else
16
+ @content_block = block
17
+ end
18
+ end
19
+
20
+ div(
21
+ **default_attrs,
22
+ data: {
23
+ toggle_target: "toggleable",
24
+ transition_enter: "transform transition ease-in-out duration-500",
25
+ transition_enter_from: "translate-x-full",
26
+ transition_enter_to: "translate-x-0",
27
+ transition_leave: "transform transition ease-in-out duration-500",
28
+ transition_leave_from: "translate-x-0",
29
+ transition_leave_to: "translate-x-full"
30
+ }
31
+ ) do
32
+ # Header with title and close button
33
+ div(class: "py-3 px-4 mb-4 bg-black") do
34
+ div(class: "flex justify-between items-center") do
35
+ h2(class: "text-base font-semibold leading-6 text-white", id: "slideover-title") do
36
+ i(class: "mr-2 fa-light fa-gear")
37
+ plain " #{@title}"
38
+ end
39
+ button(
40
+ type: "button",
41
+ data: {action: "click->toggle#toggle touch->toggle#toggle"},
42
+ class: "text-white hover:text-gray-300 transition"
43
+ ) do
44
+ i(class: "font-bold fa-regular fa-xmark right")
45
+ end
46
+ end
47
+ end
48
+
49
+ # Content area
50
+ div(class: "overflow-y-auto px-4 pb-6 space-y-6") do
51
+ if @content_html
52
+ raw(@content_html)
53
+ elsif @content_block
54
+ instance_eval(&@content_block)
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def default_attrs
63
+ {
64
+ id: "slideover",
65
+ class: slideover_classes
66
+ }
67
+ end
68
+
69
+ def slideover_classes
70
+ base = "flex absolute right-0 flex-col h-full bg-white divide-y divide-gray-200 shadow-xl basis-3/12 z-50"
71
+ visibility = @open ? "" : "hidden"
72
+ [base, visibility].compact.join(" ")
11
73
  end
12
74
  end
13
75
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class StatisticsComponent < Panda::Core::Base
7
+ prop :metric, String
8
+ prop :value, _Nilable(_Union(String, Integer, Float))
9
+
10
+ def view_template
11
+ div(class: "overflow-hidden p-4 bg-gradient-to-br rounded-lg border-2 from-light/20 to-light border-mid") do
12
+ dt(class: "text-base font-medium truncate text-dark") { @metric }
13
+ dd(class: "mt-1 text-3xl font-medium tracking-tight text-dark") { @value }
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class TabBarComponent < Panda::Core::Base
7
+ prop :tabs, Array, default: -> { [].freeze }
8
+
9
+ def view_template
10
+ div(class: "mt-3 sm:mt-2") do
11
+ render_mobile_select
12
+ render_desktop_tabs
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def render_mobile_select
19
+ div(class: "sm:hidden") do
20
+ label(for: "tabs", class: "sr-only") { "Select a tab" }
21
+ select(
22
+ id: "tabs",
23
+ name: "tabs",
24
+ class: "block py-1.5 pr-10 pl-3 w-full text-gray-900 rounded-md border-0 ring-1 ring-inset focus:ring-2 focus:ring-inset ring-mid focus:border-panda-dark focus:ring-panda-dark"
25
+ ) do
26
+ @tabs.each do |tab|
27
+ option { tab[:name] }
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def render_desktop_tabs
34
+ div(class: "hidden sm:block") do
35
+ div(class: "flex items-center border-b border-gray-200") do
36
+ nav(class: "flex flex-1 -mb-px space-x-6 xl:space-x-8", aria: {label: "Tabs"}) do
37
+ @tabs.each_with_index do |tab, index|
38
+ render_tab(tab, index.zero?)
39
+ end
40
+ end
41
+ render_view_toggle
42
+ end
43
+ end
44
+ end
45
+
46
+ def render_tab(tab, is_current = false)
47
+ classes = "py-4 px-1 text-sm font-medium whitespace-nowrap border-b-2 "
48
+ classes += if is_current || tab[:current]
49
+ "border-panda-dark text-panda-dark"
50
+ else
51
+ "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
52
+ end
53
+
54
+ a(
55
+ href: tab[:url] || "#",
56
+ class: classes,
57
+ aria: {current: (is_current || tab[:current]) ? "page" : nil}
58
+ ) { tab[:name] }
59
+ end
60
+
61
+ def render_view_toggle
62
+ div(class: "hidden items-center p-0.5 ml-6 bg-gray-100 rounded-lg sm:flex") do
63
+ render_view_button(:list)
64
+ render_view_button(:grid, selected: true)
65
+ end
66
+ end
67
+
68
+ def render_view_button(type, selected: false)
69
+ button_class = "p-1.5 text-gray-400 rounded-md focus:ring-2 focus:ring-inset focus:outline-none focus:ring-panda-dark"
70
+ button_class += if selected
71
+ " ml-0.5 bg-white shadow-sm"
72
+ else
73
+ " hover:bg-white hover:shadow-sm"
74
+ end
75
+
76
+ button(type: "button", class: button_class) do
77
+ if type == :list
78
+ svg(class: "w-5 h-5", viewBox: "0 0 20 20", fill: "currentColor", aria: {hidden: "true"}) do |s|
79
+ s.path(
80
+ fill_rule: "evenodd",
81
+ d: "M2 3.75A.75.75 0 012.75 3h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 3.75zm0 4.167a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75zm0 4.166a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75zm0 4.167a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75z",
82
+ clip_rule: "evenodd"
83
+ )
84
+ end
85
+ span(class: "sr-only") { "Use list view" }
86
+ else
87
+ svg(class: "w-5 h-5", viewBox: "0 0 20 20", fill: "currentColor", aria: {hidden: "true"}) do |s|
88
+ s.path(
89
+ fill_rule: "evenodd",
90
+ d: "M4.25 2A2.25 2.25 0 002 4.25v2.5A2.25 2.25 0 004.25 9h2.5A2.25 2.25 0 009 6.75v-2.5A2.25 2.25 0 006.75 2h-2.5zm0 9A2.25 2.25 0 002 13.25v2.5A2.25 2.25 0 004.25 18h2.5A2.25 2.25 0 009 15.75v-2.5A2.25 2.25 0 006.75 11h-2.5zm9-9A2.25 2.25 0 0011 4.25v2.5A2.25 2.25 0 0013.25 9h2.5A2.25 2.25 0 0018 6.75v-2.5A2.25 2.25 0 0015.75 2h-2.5zm0 9A2.25 2.25 0 0011 13.25v2.5A2.25 2.25 0 0013.25 18h2.5A2.25 2.25 0 0018 15.75v-2.5A2.25 2.25 0 0015.75 11h-2.5z",
91
+ clip_rule: "evenodd"
92
+ )
93
+ end
94
+ span(class: "sr-only") { "Use grid view" }
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end