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.
- checksums.yaml +4 -4
- data/app/assets/tailwind/application.css +199 -7
- data/app/assets/tailwind/tailwind.config.js +8 -0
- data/app/components/panda/core/UI/badge.rb +107 -0
- data/app/components/panda/core/UI/button.rb +110 -0
- data/app/components/panda/core/UI/card.rb +88 -0
- data/app/components/panda/core/admin/breadcrumb_component.rb +133 -0
- data/app/components/panda/core/admin/button_component.rb +46 -28
- data/app/components/panda/core/admin/container_component.rb +75 -4
- data/app/components/panda/core/admin/file_gallery_component.rb +157 -0
- data/app/components/panda/core/admin/flash_message_component.rb +98 -15
- data/app/components/panda/core/admin/form_error_component.rb +48 -0
- data/app/components/panda/core/admin/form_input_component.rb +50 -0
- data/app/components/panda/core/admin/form_select_component.rb +68 -0
- data/app/components/panda/core/admin/heading_component.rb +53 -24
- data/app/components/panda/core/admin/page_header_component.rb +107 -0
- data/app/components/panda/core/admin/panel_component.rb +33 -4
- data/app/components/panda/core/admin/slideover_component.rb +66 -4
- data/app/components/panda/core/admin/statistics_component.rb +19 -0
- data/app/components/panda/core/admin/tab_bar_component.rb +101 -0
- data/app/components/panda/core/admin/table_component.rb +92 -11
- data/app/components/panda/core/admin/tag_component.rb +58 -16
- data/app/components/panda/core/admin/user_activity_component.rb +43 -0
- data/app/components/panda/core/admin/user_display_component.rb +77 -0
- data/app/components/panda/core/base.rb +122 -0
- data/app/controllers/panda/core/admin/base_controller.rb +68 -0
- data/app/controllers/panda/core/admin/dashboard_controller.rb +5 -3
- data/app/controllers/panda/core/admin/my_profile_controller.rb +4 -4
- data/app/controllers/panda/core/admin/sessions_controller.rb +15 -8
- data/app/controllers/panda/core/admin/test_sessions_controller.rb +60 -0
- data/app/helpers/panda/core/asset_helper.rb +31 -5
- data/app/helpers/panda/core/sessions_helper.rb +27 -2
- data/app/javascript/panda/core/application.js +8 -1
- data/app/javascript/panda/core/controllers/alert_controller.js +38 -0
- data/app/javascript/panda/core/controllers/index.js +3 -3
- data/app/javascript/panda/core/controllers/toggle_controller.js +41 -0
- data/app/javascript/panda/core/tailwindplus-elements.js +31 -0
- data/app/javascript/panda/core/vendor/@hotwired--stimulus.js +4 -0
- data/app/javascript/panda/core/vendor/@hotwired--turbo.js +160 -0
- data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +4 -0
- data/app/models/panda/core/user.rb +61 -14
- data/app/services/panda/core/attach_avatar_service.rb +67 -0
- data/app/views/layouts/panda/core/admin.html.erb +40 -3
- data/app/views/layouts/panda/core/admin_simple.html.erb +6 -0
- data/app/views/panda/core/admin/dashboard/_default_content.html.erb +4 -4
- data/app/views/panda/core/admin/dashboard/show.html.erb +2 -2
- data/app/views/panda/core/admin/my_profile/edit.html.erb +36 -25
- data/app/views/panda/core/admin/sessions/new.html.erb +9 -10
- data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +27 -34
- data/app/views/panda/core/admin/shared/_flash.html.erb +4 -30
- data/app/views/panda/core/admin/shared/_sidebar.html.erb +41 -20
- data/app/views/panda/core/shared/_header.html.erb +13 -5
- data/config/importmap.rb +19 -6
- data/config/routes.rb +10 -3
- data/db/migrate/20250810120000_add_current_theme_to_panda_core_users.rb +7 -0
- data/db/migrate/20250811120000_add_oauth_avatar_url_to_panda_core_users.rb +7 -0
- data/lib/panda/core/asset_loader.rb +23 -8
- data/lib/panda/core/configuration.rb +12 -9
- data/lib/panda/core/debug.rb +47 -0
- data/lib/panda/core/engine.rb +55 -9
- data/lib/panda/core/services/base_service.rb +19 -4
- data/lib/panda/core/version.rb +1 -1
- data/lib/panda/core.rb +2 -0
- data/lib/tasks/panda_core_users.rake +158 -0
- metadata +103 -14
- data/app/components/panda/core/admin/container_component.html.erb +0 -12
- data/app/components/panda/core/admin/flash_message_component.html.erb +0 -31
- data/app/components/panda/core/admin/panel_component.html.erb +0 -7
- data/app/components/panda/core/admin/slideover_component.html.erb +0 -9
- data/app/components/panda/core/admin/table_component.html.erb +0 -29
- 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 <
|
|
7
|
-
|
|
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
|
-
|
|
12
|
+
def view_template(&block)
|
|
13
|
+
# Capture any buttons defined via block
|
|
14
|
+
instance_eval(&block) if block_given?
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
output = ""
|
|
21
|
-
output += content_tag(:div, @text, class: "grow")
|
|
35
|
+
private
|
|
22
36
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
48
|
+
def heading_classes
|
|
49
|
+
base = "flex pt-1 text-black mb-5 -mt-1"
|
|
50
|
+
styles = case @level
|
|
33
51
|
when 1
|
|
34
|
-
|
|
52
|
+
"text-2xl font-medium"
|
|
35
53
|
when 2
|
|
36
|
-
|
|
54
|
+
"text-xl font-medium"
|
|
37
55
|
when 3
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 <
|
|
7
|
-
|
|
8
|
-
|
|
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 <
|
|
7
|
-
|
|
6
|
+
class SlideoverComponent < Panda::Core::Base
|
|
7
|
+
prop :title, String, default: "Settings"
|
|
8
|
+
prop :open, _Nilable(_Boolean), default: -> { false }
|
|
8
9
|
|
|
9
|
-
def
|
|
10
|
-
|
|
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
|