panda-core 0.2.3 → 0.4.1
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/README.md +185 -0
- data/app/assets/tailwind/application.css +279 -0
- data/app/assets/tailwind/tailwind.config.js +21 -0
- data/app/components/panda/core/UI/badge.rb +107 -0
- data/app/components/panda/core/UI/button.rb +89 -0
- data/app/components/panda/core/UI/card.rb +88 -0
- data/app/components/panda/core/admin/button_component.rb +46 -28
- data/app/components/panda/core/admin/container_component.rb +52 -4
- data/app/components/panda/core/admin/flash_message_component.rb +74 -9
- 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 +52 -24
- data/app/components/panda/core/admin/panel_component.rb +33 -4
- data/app/components/panda/core/admin/slideover_component.rb +8 -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 +90 -9
- data/app/components/panda/core/admin/tag_component.rb +21 -16
- data/app/components/panda/core/admin/user_activity_component.rb +43 -0
- data/app/components/panda/core/admin/user_display_component.rb +78 -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 +7 -6
- data/app/controllers/panda/core/admin/my_profile_controller.rb +3 -3
- data/app/controllers/panda/core/admin/sessions_controller.rb +26 -5
- data/app/helpers/panda/core/sessions_helper.rb +21 -0
- data/app/javascript/panda/core/application.js +1 -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 +17 -13
- data/app/views/layouts/panda/core/admin.html.erb +40 -57
- data/app/views/layouts/panda/core/admin_simple.html.erb +5 -0
- data/app/views/panda/core/admin/dashboard/_default_content.html.erb +73 -0
- data/app/views/panda/core/admin/dashboard/show.html.erb +4 -10
- data/app/views/panda/core/admin/my_profile/edit.html.erb +13 -27
- data/app/views/panda/core/admin/sessions/new.html.erb +13 -12
- 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 +36 -20
- data/app/views/panda/core/shared/_footer.html.erb +2 -0
- data/app/views/panda/core/shared/_header.html.erb +19 -0
- data/config/importmap.rb +15 -0
- data/config/initializers/panda_core.rb +37 -1
- data/config/routes.rb +7 -7
- data/db/migrate/20250810120000_add_current_theme_to_panda_core_users.rb +7 -0
- data/lib/generators/panda/core/install_generator.rb +3 -9
- data/lib/generators/panda/core/templates/README +25 -0
- data/lib/generators/panda/core/templates/initializer.rb +28 -0
- data/lib/panda/core/asset_loader.rb +23 -8
- data/lib/panda/core/configuration.rb +41 -9
- data/lib/panda/core/debug.rb +47 -0
- data/lib/panda/core/engine.rb +82 -8
- data/lib/panda/core/version.rb +1 -1
- data/lib/panda/core.rb +1 -0
- data/lib/tasks/assets.rake +58 -392
- data/lib/tasks/panda_core_tasks.rake +16 -0
- metadata +102 -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 -28
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module UI
|
|
6
|
+
# Card component for containing related content.
|
|
7
|
+
#
|
|
8
|
+
# Cards are flexible containers that can hold any content,
|
|
9
|
+
# with optional padding, shadows, and border variations.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic card
|
|
12
|
+
# render Panda::Core::UI::Card.new do
|
|
13
|
+
# "Card content here"
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# @example Card with header and footer
|
|
17
|
+
# render Panda::Core::UI::Card.new(padding: :large) do |card|
|
|
18
|
+
# card.with_header { h3 { "Card Title" } }
|
|
19
|
+
# card.with_body { p { "Main content" } }
|
|
20
|
+
# card.with_footer { "Footer content" }
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @example Elevated card with no padding
|
|
24
|
+
# render Panda::Core::UI::Card.new(
|
|
25
|
+
# elevation: :high,
|
|
26
|
+
# padding: :none
|
|
27
|
+
# ) do
|
|
28
|
+
# img(src: "/image.jpg", alt: "Card image")
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
class Card < Panda::Core::Base
|
|
32
|
+
prop :padding, Symbol, default: :medium
|
|
33
|
+
prop :elevation, Symbol, default: :low
|
|
34
|
+
prop :border, _Boolean, default: true
|
|
35
|
+
|
|
36
|
+
def view_template(&block)
|
|
37
|
+
div(**@attrs, &block)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def default_attrs
|
|
41
|
+
{
|
|
42
|
+
class: card_classes
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def card_classes
|
|
49
|
+
base = "bg-white rounded-lg overflow-hidden"
|
|
50
|
+
base += " #{padding_classes}"
|
|
51
|
+
base += " #{elevation_classes}"
|
|
52
|
+
base += " #{border_classes}"
|
|
53
|
+
base
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def padding_classes
|
|
57
|
+
case padding
|
|
58
|
+
when :none
|
|
59
|
+
""
|
|
60
|
+
when :small, :sm
|
|
61
|
+
"p-4"
|
|
62
|
+
when :large, :lg
|
|
63
|
+
"p-8"
|
|
64
|
+
else # :medium, :md
|
|
65
|
+
"p-6"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def elevation_classes
|
|
70
|
+
case elevation
|
|
71
|
+
when :none
|
|
72
|
+
""
|
|
73
|
+
when :medium, :md
|
|
74
|
+
"shadow-md"
|
|
75
|
+
when :high, :lg
|
|
76
|
+
"shadow-lg"
|
|
77
|
+
else # :low, :sm
|
|
78
|
+
"shadow-sm"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def border_classes
|
|
83
|
+
border ? "border border-gray-200" : ""
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -3,56 +3,74 @@
|
|
|
3
3
|
module Panda
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
|
-
class ButtonComponent <
|
|
7
|
-
|
|
6
|
+
class ButtonComponent < Panda::Core::Base
|
|
7
|
+
prop :text, String, default: "Button"
|
|
8
|
+
prop :action, _Nilable(Symbol), default: -> {}
|
|
9
|
+
prop :href, String, default: "#"
|
|
10
|
+
prop :icon, _Nilable(String), default: -> {}
|
|
11
|
+
prop :size, Symbol, default: :regular
|
|
12
|
+
prop :id, _Nilable(String), default: -> {}
|
|
8
13
|
|
|
9
|
-
def
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
def view_template
|
|
15
|
+
a(**@attrs) do
|
|
16
|
+
if computed_icon
|
|
17
|
+
i(class: "mr-2 fa-regular fa-#{computed_icon}")
|
|
18
|
+
plain " "
|
|
19
|
+
end
|
|
20
|
+
plain @text.titleize
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def default_attrs
|
|
25
|
+
{
|
|
26
|
+
href: @href,
|
|
27
|
+
class: button_classes,
|
|
28
|
+
id: @id
|
|
29
|
+
}
|
|
17
30
|
end
|
|
18
31
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def computed_icon
|
|
35
|
+
@computed_icon ||= @icon || icon_from_action(@action)
|
|
36
|
+
end
|
|
23
37
|
|
|
24
|
-
|
|
38
|
+
def button_classes
|
|
39
|
+
base = "inline-flex items-center rounded-md font-medium shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
|
|
40
|
+
base + size_classes + action_classes
|
|
41
|
+
end
|
|
25
42
|
|
|
43
|
+
def size_classes
|
|
26
44
|
case @size
|
|
27
45
|
when :small, :sm
|
|
28
|
-
|
|
46
|
+
"gap-x-1.5 px-2.5 py-1.5 text-sm "
|
|
29
47
|
when :medium, :regular, :md
|
|
30
|
-
|
|
48
|
+
"gap-x-1.5 px-3 py-2 text-base "
|
|
31
49
|
when :large, :lg
|
|
32
|
-
|
|
50
|
+
"gap-x-2 px-3.5 py-2.5 text-lg "
|
|
51
|
+
else
|
|
52
|
+
"gap-x-1.5 px-3 py-2 text-base "
|
|
33
53
|
end
|
|
54
|
+
end
|
|
34
55
|
|
|
35
|
-
|
|
56
|
+
def action_classes
|
|
57
|
+
case @action
|
|
36
58
|
when :save, :create
|
|
37
59
|
"text-white bg-green-600 hover:bg-green-700"
|
|
38
60
|
when :save_inactive
|
|
39
61
|
"text-white bg-gray-400"
|
|
40
62
|
when :secondary
|
|
41
|
-
"text-gray-700 border-2 border-gray-700 bg-transparent hover:bg-gray-100 transition-all
|
|
63
|
+
"text-gray-700 border-2 border-gray-700 bg-transparent hover:bg-gray-100 transition-all"
|
|
42
64
|
when :delete, :destroy, :danger
|
|
43
|
-
"text-red-600 border border-red-600 bg-red-100 hover:bg-red-200 hover:text-red-700 focus-visible:outline-red-300
|
|
65
|
+
"text-red-600 border border-red-600 bg-red-100 hover:bg-red-200 hover:text-red-700 focus-visible:outline-red-300"
|
|
44
66
|
else
|
|
45
|
-
"text-gray-700 border-2 border-gray-700 bg-transparent hover:bg-gray-100 transition-all
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
content_tag :a, href: @link, class: classes, data: @data, id: @id do
|
|
49
|
-
@text
|
|
67
|
+
"text-gray-700 border-2 border-gray-700 bg-transparent hover:bg-gray-100 transition-all"
|
|
50
68
|
end
|
|
51
69
|
end
|
|
52
70
|
|
|
53
|
-
|
|
71
|
+
def icon_from_action(action)
|
|
72
|
+
return nil unless action
|
|
54
73
|
|
|
55
|
-
def set_icon_from_action(action)
|
|
56
74
|
case action
|
|
57
75
|
when :add, :new, :create
|
|
58
76
|
"plus"
|
|
@@ -3,10 +3,58 @@
|
|
|
3
3
|
module Panda
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
|
-
class ContainerComponent <
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
class ContainerComponent < 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
|
+
main(class: "overflow-auto flex-1 h-full min-h-full max-h-full") do
|
|
20
|
+
div(class: "overflow-auto px-2 pt-4 mx-auto sm:px-6 lg:px-6") do
|
|
21
|
+
@heading_content&.call
|
|
22
|
+
@tab_bar_content&.call
|
|
23
|
+
|
|
24
|
+
section(class: "flex-auto") do
|
|
25
|
+
div(class: "flex-1 mt-4 w-full") do
|
|
26
|
+
if @main_content
|
|
27
|
+
@main_content.call
|
|
28
|
+
elsif @body_html
|
|
29
|
+
raw(@body_html)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
@slideover_content&.call
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def content(&block)
|
|
39
|
+
@main_content = if defined?(view_context) && view_context
|
|
40
|
+
# Capture ERB content
|
|
41
|
+
-> { raw(view_context.capture(&block)) }
|
|
42
|
+
else
|
|
43
|
+
block
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def heading(**props, &block)
|
|
48
|
+
@heading_content = -> { render(Panda::Core::Admin::HeadingComponent.new(**props), &block) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def tab_bar(**props, &block)
|
|
52
|
+
@tab_bar_content = -> { render(Panda::Core::Admin::TabBarComponent.new(**props), &block) } if defined?(Panda::Core::Admin::TabBarComponent)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def slideover(**props, &block)
|
|
56
|
+
@slideover_content = -> { render(Panda::Core::Admin::SlideoverComponent.new(**props), &block) }
|
|
57
|
+
end
|
|
10
58
|
end
|
|
11
59
|
end
|
|
12
60
|
end
|
|
@@ -3,17 +3,82 @@
|
|
|
3
3
|
module Panda
|
|
4
4
|
module Core
|
|
5
5
|
module Admin
|
|
6
|
-
class FlashMessageComponent < ::
|
|
7
|
-
|
|
6
|
+
class FlashMessageComponent < Panda::Core::Base
|
|
7
|
+
prop :message, String
|
|
8
|
+
prop :kind, Symbol
|
|
9
|
+
prop :temporary, _Boolean, default: true
|
|
8
10
|
|
|
9
|
-
def
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
def view_template
|
|
12
|
+
div(**container_attrs) do
|
|
13
|
+
div(class: "overflow-hidden w-full max-w-sm bg-white rounded-lg ring-1 ring-black ring-opacity-5 shadow-lg") do
|
|
14
|
+
div(class: "p-4") do
|
|
15
|
+
div(class: "flex items-start") do
|
|
16
|
+
render_icon
|
|
17
|
+
render_content
|
|
18
|
+
render_close_button
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def container_attrs
|
|
28
|
+
attrs = {
|
|
29
|
+
class: "fixed top-2 right-2 z-[9999] p-2 space-y-4 w-full max-w-sm sm:items-end",
|
|
30
|
+
data: {
|
|
31
|
+
controller: "alert",
|
|
32
|
+
transition_enter: "ease-in-out duration-500",
|
|
33
|
+
transition_enter_from: "translate-x-full opacity-0",
|
|
34
|
+
transition_enter_to: "translate-x-0 opacity-100",
|
|
35
|
+
transition_leave: "ease-in-out duration-500",
|
|
36
|
+
transition_leave_from: "translate-x-0 opacity-100",
|
|
37
|
+
transition_leave_to: "translate-x-full opacity-0"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
attrs[:data][:alert_dismiss_after_value] = "3000" if @temporary
|
|
42
|
+
attrs
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def render_icon
|
|
46
|
+
div(class: "flex-shrink-0") do
|
|
47
|
+
i(class: "fa-regular text-xl #{icon_css} #{text_colour_css}")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def render_content
|
|
52
|
+
div(class: "flex-1 pt-0.5 ml-3 w-0") do
|
|
53
|
+
p(class: "mb-1 text-sm font-medium flash-message-title #{text_colour_css}") { @kind.to_s.titleize }
|
|
54
|
+
p(class: "mt-1 mb-0 text-sm text-gray-500 flash-message-text") { @message }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def render_close_button
|
|
59
|
+
div(class: "flex flex-shrink-0 ml-4") do
|
|
60
|
+
button(
|
|
61
|
+
type: "button",
|
|
62
|
+
class: "inline-flex text-gray-400 bg-white rounded-md transition duration-150 ease-in-out hover:text-gray-500 focus:ring-2 focus:ring-offset-2 focus:outline-none focus:ring-sky-500",
|
|
63
|
+
data: {action: "alert#close"}
|
|
64
|
+
) do
|
|
65
|
+
span(class: "sr-only") { "Close" }
|
|
66
|
+
svg(
|
|
67
|
+
class: "w-5 h-5",
|
|
68
|
+
viewBox: "0 0 20 20",
|
|
69
|
+
fill: "currentColor",
|
|
70
|
+
aria: {hidden: "true"}
|
|
71
|
+
) do |s|
|
|
72
|
+
s.path(
|
|
73
|
+
d: "M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
13
78
|
end
|
|
14
79
|
|
|
15
80
|
def text_colour_css
|
|
16
|
-
case kind
|
|
81
|
+
case @kind
|
|
17
82
|
when :success
|
|
18
83
|
"text-green-600"
|
|
19
84
|
when :alert, :error
|
|
@@ -28,10 +93,10 @@ module Panda
|
|
|
28
93
|
end
|
|
29
94
|
|
|
30
95
|
def icon_css
|
|
31
|
-
case kind
|
|
96
|
+
case @kind
|
|
32
97
|
when :success
|
|
33
98
|
"fa-circle-check"
|
|
34
|
-
when :alert
|
|
99
|
+
when :alert, :error
|
|
35
100
|
"fa-circle-xmark"
|
|
36
101
|
when :warning
|
|
37
102
|
"fa-triangle-exclamation"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class FormErrorComponent < Panda::Core::Base
|
|
7
|
+
prop :errors, _Nilable(_Union(ActiveModel::Errors, Array)), default: -> {}
|
|
8
|
+
prop :model, _Nilable(Object), default: -> {}
|
|
9
|
+
|
|
10
|
+
def view_template
|
|
11
|
+
return unless should_render?
|
|
12
|
+
|
|
13
|
+
div(**@attrs) do
|
|
14
|
+
div(class: "text-sm text-red-600") do
|
|
15
|
+
error_messages.each do |message|
|
|
16
|
+
p { message }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def default_attrs
|
|
23
|
+
{
|
|
24
|
+
class: "mb-4 p-4 bg-red-50 border border-red-200 rounded-md"
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def should_render?
|
|
31
|
+
error_messages.any?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def error_messages
|
|
35
|
+
@error_messages ||= if @model&.respond_to?(:errors)
|
|
36
|
+
@model.errors.full_messages
|
|
37
|
+
elsif @errors.is_a?(ActiveModel::Errors)
|
|
38
|
+
@errors.full_messages
|
|
39
|
+
elsif @errors.is_a?(Array)
|
|
40
|
+
@errors
|
|
41
|
+
else
|
|
42
|
+
[]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -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,70 @@
|
|
|
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) { render_content }
|
|
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
|
-
|
|
21
|
-
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def render_content
|
|
38
|
+
div(class: "grow") { @text }
|
|
22
39
|
|
|
23
|
-
if buttons?
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
if @buttons&.any?
|
|
41
|
+
span(class: "actions flex gap-x-2 -mt-1") do
|
|
42
|
+
@buttons.each { |btn| render(btn) }
|
|
26
43
|
end
|
|
27
44
|
end
|
|
45
|
+
end
|
|
28
46
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
case level
|
|
47
|
+
def heading_classes
|
|
48
|
+
base = "flex pt-1 text-black mb-5 -mt-1"
|
|
49
|
+
styles = case @level
|
|
33
50
|
when 1
|
|
34
|
-
|
|
51
|
+
"text-2xl font-medium"
|
|
35
52
|
when 2
|
|
36
|
-
|
|
53
|
+
"text-xl font-medium"
|
|
37
54
|
when 3
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
"text-xl font-light"
|
|
56
|
+
else
|
|
57
|
+
"text-xl font-medium"
|
|
41
58
|
end
|
|
59
|
+
|
|
60
|
+
[base, styles, *additional_styles_array].compact.join(" ")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def panel_heading_classes
|
|
64
|
+
"text-base font-medium p-4 text-white"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def additional_styles_array
|
|
68
|
+
return [] if @additional_styles.blank?
|
|
69
|
+
@additional_styles.is_a?(String) ? @additional_styles.split(" ") : @additional_styles
|
|
42
70
|
end
|
|
43
71
|
end
|
|
44
72
|
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-gray-500 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,15 @@
|
|
|
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
8
|
|
|
9
|
-
def
|
|
10
|
-
|
|
9
|
+
def view_template(&block)
|
|
10
|
+
# Set content_for equivalents that can be accessed by the layout
|
|
11
|
+
helpers.content_for(:sidebar_title) { title }
|
|
12
|
+
helpers.content_for(:sidebar) do
|
|
13
|
+
aside(class: "hidden overflow-y-auto w-96 h-full bg-white lg:block", &block)
|
|
14
|
+
end
|
|
11
15
|
end
|
|
12
16
|
end
|
|
13
17
|
end
|