panda-core 0.2.4 → 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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/tailwind/application.css +104 -7
  3. data/app/components/panda/core/UI/badge.rb +107 -0
  4. data/app/components/panda/core/UI/button.rb +89 -0
  5. data/app/components/panda/core/UI/card.rb +88 -0
  6. data/app/components/panda/core/admin/button_component.rb +46 -28
  7. data/app/components/panda/core/admin/container_component.rb +52 -4
  8. data/app/components/panda/core/admin/flash_message_component.rb +74 -9
  9. data/app/components/panda/core/admin/form_error_component.rb +48 -0
  10. data/app/components/panda/core/admin/form_input_component.rb +50 -0
  11. data/app/components/panda/core/admin/form_select_component.rb +68 -0
  12. data/app/components/panda/core/admin/heading_component.rb +52 -24
  13. data/app/components/panda/core/admin/panel_component.rb +33 -4
  14. data/app/components/panda/core/admin/slideover_component.rb +8 -4
  15. data/app/components/panda/core/admin/statistics_component.rb +19 -0
  16. data/app/components/panda/core/admin/tab_bar_component.rb +101 -0
  17. data/app/components/panda/core/admin/table_component.rb +90 -9
  18. data/app/components/panda/core/admin/tag_component.rb +21 -16
  19. data/app/components/panda/core/admin/user_activity_component.rb +43 -0
  20. data/app/components/panda/core/admin/user_display_component.rb +78 -0
  21. data/app/components/panda/core/base.rb +122 -0
  22. data/app/controllers/panda/core/admin/base_controller.rb +68 -0
  23. data/app/controllers/panda/core/admin/dashboard_controller.rb +5 -3
  24. data/app/controllers/panda/core/admin/my_profile_controller.rb +3 -3
  25. data/app/controllers/panda/core/admin/sessions_controller.rb +9 -6
  26. data/app/helpers/panda/core/sessions_helper.rb +1 -1
  27. data/app/javascript/panda/core/vendor/@hotwired--stimulus.js +4 -0
  28. data/app/javascript/panda/core/vendor/@hotwired--turbo.js +160 -0
  29. data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +4 -0
  30. data/app/models/panda/core/user.rb +17 -13
  31. data/app/views/layouts/panda/core/admin.html.erb +40 -3
  32. data/app/views/layouts/panda/core/admin_simple.html.erb +5 -0
  33. data/app/views/panda/core/admin/dashboard/_default_content.html.erb +4 -4
  34. data/app/views/panda/core/admin/dashboard/show.html.erb +2 -2
  35. data/app/views/panda/core/admin/my_profile/edit.html.erb +13 -27
  36. data/app/views/panda/core/admin/sessions/new.html.erb +7 -7
  37. data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +27 -34
  38. data/app/views/panda/core/admin/shared/_flash.html.erb +4 -30
  39. data/app/views/panda/core/admin/shared/_sidebar.html.erb +36 -20
  40. data/app/views/panda/core/shared/_header.html.erb +13 -5
  41. data/config/importmap.rb +11 -6
  42. data/config/routes.rb +2 -4
  43. data/db/migrate/20250810120000_add_current_theme_to_panda_core_users.rb +7 -0
  44. data/lib/panda/core/asset_loader.rb +23 -8
  45. data/lib/panda/core/configuration.rb +12 -9
  46. data/lib/panda/core/debug.rb +47 -0
  47. data/lib/panda/core/engine.rb +43 -6
  48. data/lib/panda/core/version.rb +1 -1
  49. data/lib/panda/core.rb +1 -0
  50. metadata +93 -14
  51. data/app/components/panda/core/admin/container_component.html.erb +0 -12
  52. data/app/components/panda/core/admin/flash_message_component.html.erb +0 -31
  53. data/app/components/panda/core/admin/panel_component.html.erb +0 -7
  54. data/app/components/panda/core/admin/slideover_component.html.erb +0 -9
  55. data/app/components/panda/core/admin/table_component.html.erb +0 -29
  56. data/app/controllers/panda/core/admin_controller.rb +0 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea69195a180331f2758bbae8cfc22ccae44cf8b8ac0133b0642c8e4fb7e11e41
4
- data.tar.gz: 495cd12223dc77a2c3893568732ae8ae73374bc0bdaf940cf4498227eae082dc
3
+ metadata.gz: 1571548de89ff0ad16010f4500ed47a1c53dda70c730945fa439272a33567999
4
+ data.tar.gz: 02cc839e0831b886a37729b43a5d185d1bd7a13eb3ff59dd2e3afc0f7996e4cd
5
5
  SHA512:
6
- metadata.gz: eb217e7a4b3f7ddfee0e9e13e252c5fe31a66535b1b239badf9b0b1bfcd71906effd222b0eb2457b6bd4cd309ba69ac8f7edca6db675f93661b7706969728267
7
- data.tar.gz: 7b39637eb2c83328f84f349ff0006b550741d9c91afacdbdc5c9f11102dc25baf58ae24dd514b87891ec1a2c1d0bfa3fee7b623d919a0113ef4644ea2d3d2a62
6
+ metadata.gz: acaebae64c9e8b970edda07e35f0b3820e988d3c738c3cf0c4d9c647be828d98773992d6a1ab70a4dc74cc01edaa04582a43c0fa285222a2e2d8169afaba7ac0
7
+ data.tar.gz: 927db64e6429b4687667e8e47b185abea96e1f50668b5e6abe2d36050a1606aac880610f94d2f08205fb4a1280e3c95f05f8c45a8afc1d03ca10a38fad0abdd5
@@ -1,4 +1,12 @@
1
- @import "tailwindcss";
1
+ @import 'tailwindcss';
2
+
3
+ @source "../../app/views/**/*.html.erb";
4
+ @source "../../app/components/**/*.html.erb";
5
+ @source "../../app/components/**/*.rb";
6
+ @source "../../app/helpers/**/*.rb";
7
+ @source "../../../cms/app/views/**/*.html.erb";
8
+ @source "../../../cms/app/components/**/*.html.erb";
9
+ @source "../../../cms/app/components/**/*.rb";
2
10
 
3
11
  @theme {
4
12
  --color-white: var(--color-white);
@@ -14,7 +22,7 @@
14
22
  }
15
23
 
16
24
  @layer base {
17
- html[data-theme="default"] {
25
+ html[data-theme='default'] {
18
26
  --color-white: rgb(249, 249, 249); /* #F9F9F9 */
19
27
  --color-black: rgb(26, 22, 29); /* #1A161D */
20
28
 
@@ -30,7 +38,7 @@
30
38
  --color-error: rgb(245, 129, 129); /* #F58181 */
31
39
  }
32
40
 
33
- html[data-theme="sky"] {
41
+ html[data-theme='sky'] {
34
42
  --color-white: rgb(249, 249, 249); /* #F9F9F9 */
35
43
  --color-black: rgb(26, 22, 29); /* #1A161D */
36
44
  --color-light: rgb(204, 238, 242); /* #CCEEF2 */
@@ -46,20 +54,109 @@
46
54
 
47
55
  a.block-link:after {
48
56
  position: absolute;
49
- content: "";
57
+ content: '';
50
58
  inset: 0;
51
59
  }
52
60
 
53
61
  /* Admin gradient backgrounds */
54
- html[data-theme="default"] .bg-gradient-admin {
62
+ html[data-theme='default'] .bg-gradient-admin {
55
63
  background: linear-gradient(to bottom right, rgb(33, 29, 73), rgb(141, 94, 183));
56
64
  }
57
65
 
58
- html[data-theme="sky"] .bg-gradient-admin {
66
+ html[data-theme='sky'] .bg-gradient-admin {
59
67
  background: linear-gradient(to bottom right, rgb(20, 32, 74), rgb(42, 102, 159));
60
68
  }
61
69
  }
62
70
 
71
+ /* Form input styles */
72
+ @layer components {
73
+ /* Base form field styles - matches panda-cms admin pattern */
74
+ input[type='text'],
75
+ input[type='email'],
76
+ input[type='password'],
77
+ input[type='url'],
78
+ input[type='tel'],
79
+ input[type='number'],
80
+ input[type='date'],
81
+ input[type='datetime-local'],
82
+ input[type='month'],
83
+ input[type='week'],
84
+ input[type='time'],
85
+ input[type='search'],
86
+ textarea {
87
+ @apply block w-full rounded-md border-0 p-2 text-gray-900 bg-white;
88
+ @apply ring-1 ring-inset ring-mid placeholder:text-gray-300;
89
+ @apply focus:ring-1 focus:ring-inset focus:ring-dark;
90
+ @apply hover:cursor-pointer sm:leading-6;
91
+ @apply disabled:ring-gray-300 disabled:focus:ring-gray-300 disabled:bg-gray-50 disabled:cursor-not-allowed;
92
+ }
93
+
94
+ /* Select specific styling - matches panda-cms admin pattern */
95
+ select {
96
+ @apply block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 bg-white;
97
+ @apply ring-1 ring-inset ring-mid;
98
+ @apply focus:ring-1 focus:ring-inset focus:ring-dark;
99
+ @apply hover:cursor-pointer sm:leading-6;
100
+ @apply disabled:ring-gray-300 disabled:focus:ring-gray-300 disabled:bg-gray-50 disabled:cursor-not-allowed;
101
+ @apply appearance-none bg-right bg-no-repeat;
102
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
103
+ background-position: right 0.5rem center;
104
+ background-size: 1.5em 1.5em;
105
+ }
106
+
107
+ /* Checkbox and radio styling */
108
+ input[type='checkbox'],
109
+ input[type='radio'] {
110
+ @apply w-4 h-4 text-indigo-600 bg-white border-gray-300 rounded transition-colors duration-200;
111
+ @apply focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2;
112
+ @apply disabled:bg-gray-100 disabled:cursor-not-allowed;
113
+ }
114
+
115
+ input[type='radio'] {
116
+ @apply rounded-full;
117
+ }
118
+
119
+ /* Label styling */
120
+ label {
121
+ @apply block text-sm font-medium text-gray-700 mb-1;
122
+ }
123
+
124
+ /* Field wrapper styling */
125
+ .field {
126
+ @apply mb-4;
127
+ }
128
+
129
+ /* Error state styling */
130
+ input.error,
131
+ textarea.error,
132
+ select.error {
133
+ @apply ring-red-500 focus:ring-red-500;
134
+ }
135
+
136
+ .field-error {
137
+ @apply text-sm text-red-600 mt-1;
138
+ }
139
+
140
+ /* Button styling */
141
+ .btn {
142
+ @apply inline-flex items-center justify-center px-6 py-3 text-base font-semibold rounded-md transition-colors duration-200;
143
+ @apply focus:outline-none focus:ring-2 focus:ring-offset-2;
144
+ @apply disabled:opacity-50 disabled:cursor-not-allowed;
145
+ }
146
+
147
+ .btn-primary {
148
+ @apply bg-mid text-white hover:opacity-90 focus:ring-mid;
149
+ }
150
+
151
+ .btn-secondary {
152
+ @apply bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500;
153
+ }
154
+
155
+ .btn-danger {
156
+ @apply bg-red-600 text-white hover:bg-red-700 focus:ring-red-500;
157
+ }
158
+ }
159
+
63
160
  /* EditorJS content styles */
64
161
  @layer components {
65
162
  .codex-editor__redactor .ce-block .ce-block__content {
@@ -103,7 +200,7 @@
103
200
  }
104
201
 
105
202
  .cdx-quote__text {
106
- quotes: "\201C" "\201D" "\2018" "\2019";
203
+ quotes: '\201C' '\201D' '\2018' '\2019';
107
204
  @apply pl-6;
108
205
 
109
206
  &:before {
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module UI
6
+ # Badge component for status indicators, labels, and counts.
7
+ #
8
+ # Badges are small, inline elements that highlight an item's status
9
+ # or provide additional metadata at a glance.
10
+ #
11
+ # @example Basic badge
12
+ # render Panda::Core::UI::Badge.new(text: "New")
13
+ #
14
+ # @example Status badges
15
+ # render Panda::Core::UI::Badge.new(text: "Active", variant: :success)
16
+ # render Panda::Core::UI::Badge.new(text: "Pending", variant: :warning)
17
+ # render Panda::Core::UI::Badge.new(text: "Error", variant: :danger)
18
+ #
19
+ # @example With count
20
+ # render Panda::Core::UI::Badge.new(text: "99+", variant: :primary, size: :small)
21
+ #
22
+ # @example Removable badge
23
+ # render Panda::Core::UI::Badge.new(
24
+ # text: "Tag",
25
+ # removable: true,
26
+ # data: { action: "click->tags#remove" }
27
+ # )
28
+ #
29
+ class Badge < Panda::Core::Base
30
+ prop :text, String
31
+ prop :variant, Symbol, default: :default
32
+ prop :size, Symbol, default: :medium
33
+ prop :removable, _Boolean, default: false
34
+ prop :rounded, _Boolean, default: false
35
+
36
+ def view_template
37
+ span(**@attrs) do
38
+ plain text
39
+ if removable
40
+ whitespace
41
+ button(
42
+ type: "button",
43
+ class: "inline-flex items-center ml-1 hover:opacity-70",
44
+ aria: {label: "Remove"}
45
+ ) do
46
+ svg(
47
+ class: "h-3 w-3",
48
+ xmlns: "http://www.w3.org/2000/svg",
49
+ viewBox: "0 0 20 20",
50
+ fill: "currentColor"
51
+ ) do |s|
52
+ s.path(
53
+ 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"
54
+ )
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ def default_attrs
62
+ {
63
+ class: badge_classes
64
+ }
65
+ end
66
+
67
+ private
68
+
69
+ def badge_classes
70
+ base = "inline-flex items-center font-medium"
71
+ base += " #{size_classes}"
72
+ base += " #{variant_classes}"
73
+ base += rounded ? " rounded-full" : " rounded"
74
+ base
75
+ end
76
+
77
+ def size_classes
78
+ case size
79
+ when :small, :sm
80
+ "px-2 py-0.5 text-xs"
81
+ when :large, :lg
82
+ "px-3 py-1 text-base"
83
+ else # :medium, :md
84
+ "px-2.5 py-0.5 text-sm"
85
+ end
86
+ end
87
+
88
+ def variant_classes
89
+ case variant
90
+ when :primary
91
+ "bg-blue-50 text-blue-700 ring-1 ring-inset ring-blue-700/10"
92
+ when :success
93
+ "bg-green-50 text-green-700 ring-1 ring-inset ring-green-600/20"
94
+ when :warning
95
+ "bg-yellow-50 text-yellow-800 ring-1 ring-inset ring-yellow-600/20"
96
+ when :danger
97
+ "bg-red-50 text-red-700 ring-1 ring-inset ring-red-600/10"
98
+ when :info
99
+ "bg-sky-50 text-sky-700 ring-1 ring-inset ring-sky-700/10"
100
+ else # :default
101
+ "bg-gray-50 text-gray-600 ring-1 ring-inset ring-gray-500/10"
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module UI
6
+ # Modern Phlex-based button component with type-safe props.
7
+ #
8
+ # This component demonstrates the recommended pattern for building
9
+ # Phlex components in the Panda ecosystem using the shared base class.
10
+ #
11
+ # @example Basic usage
12
+ # render Panda::Core::UI::Button.new(text: "Click me")
13
+ #
14
+ # @example With variant
15
+ # render Panda::Core::UI::Button.new(
16
+ # text: "Delete",
17
+ # variant: :danger,
18
+ # size: :large
19
+ # )
20
+ #
21
+ # @example With custom attributes
22
+ # render Panda::Core::UI::Button.new(
23
+ # text: "Submit",
24
+ # variant: :primary,
25
+ # class: "mt-4",
26
+ # data: { turbo_method: :post }
27
+ # )
28
+ #
29
+ class Button < Panda::Core::Base
30
+ # Type-safe properties using Literal
31
+ prop :text, String
32
+ prop :variant, Symbol, default: :default
33
+ prop :size, Symbol, default: :medium
34
+ prop :disabled, _Boolean, default: false
35
+ prop :type, String, default: "button"
36
+
37
+ def view_template
38
+ button(**@attrs) { text }
39
+ end
40
+
41
+ def default_attrs
42
+ {
43
+ type: type,
44
+ disabled: disabled,
45
+ class: button_classes
46
+ }
47
+ end
48
+
49
+ private
50
+
51
+ def button_classes
52
+ base = "inline-flex items-center rounded-md font-medium shadow-sm transition-colors"
53
+ base += " #{size_classes}"
54
+ base += " #{variant_classes}"
55
+ base += " disabled:opacity-50 disabled:cursor-not-allowed" if disabled
56
+ base
57
+ end
58
+
59
+ def size_classes
60
+ case size
61
+ when :small, :sm
62
+ "gap-x-1.5 px-2.5 py-1.5 text-sm"
63
+ when :large, :lg
64
+ "gap-x-2 px-3.5 py-2.5 text-lg"
65
+ else # :medium, :md
66
+ "gap-x-1.5 px-3 py-2 text-base"
67
+ end
68
+ end
69
+
70
+ def variant_classes
71
+ case variant
72
+ when :primary
73
+ "bg-blue-600 text-white hover:bg-blue-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
74
+ when :secondary
75
+ "bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
76
+ when :success
77
+ "bg-green-600 text-white hover:bg-green-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600"
78
+ when :danger
79
+ "bg-red-600 text-white hover:bg-red-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
80
+ when :ghost
81
+ "bg-transparent text-gray-700 hover:bg-gray-100"
82
+ else # :default
83
+ "bg-gray-700 text-white hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-700"
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -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 < ViewComponent::Base
7
- attr_accessor :text, :action, :link, :icon, :size, :data
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 initialize(text: "Button", action: nil, data: {}, link: "#", icon: nil, size: :regular, id: nil)
10
- @text = text
11
- @action = action
12
- @data = data
13
- @link = link
14
- @icon = icon
15
- @size = size
16
- @id = id
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
- def call
20
- @icon = set_icon_from_action(@action) if @action && @icon.nil?
21
- icon = content_tag(:i, "", class: "mr-2 fa-regular fa-#{@icon}") if @icon
22
- @text = "#{icon} #{@text.titleize}".html_safe
32
+ private
33
+
34
+ def computed_icon
35
+ @computed_icon ||= @icon || icon_from_action(@action)
36
+ end
23
37
 
24
- classes = "inline-flex items-center rounded-md font-medium shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
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
- classes += "gap-x-1.5 px-2.5 py-1.5 text-sm "
46
+ "gap-x-1.5 px-2.5 py-1.5 text-sm "
29
47
  when :medium, :regular, :md
30
- classes += "gap-x-1.5 px-3 py-2 text-base "
48
+ "gap-x-1.5 px-3 py-2 text-base "
31
49
  when :large, :lg
32
- classes += "gap-x-2 px-3.5 py-2.5 text-lg "
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
- classes += case @action
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
- private
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 < ViewComponent::Base
7
- renders_one :heading, "Panda::Core::Admin::HeadingComponent"
8
- renders_one :tab_bar, "Panda::Core::Admin::TabBarComponent"
9
- renders_one :slideover, "Panda::Core::Admin::SlideoverComponent"
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