panda-core 0.4.1 → 0.7.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 +95 -0
- data/app/assets/tailwind/tailwind.config.js +8 -0
- data/app/builders/panda/core/form_builder.rb +163 -11
- data/app/components/panda/core/UI/button.rb +45 -24
- data/app/components/panda/core/admin/breadcrumb_component.rb +133 -0
- data/app/components/panda/core/admin/button_component.rb +27 -12
- data/app/components/panda/core/admin/container_component.rb +40 -5
- data/app/components/panda/core/admin/file_gallery_component.rb +157 -0
- data/app/components/panda/core/admin/flash_message_component.rb +54 -36
- data/app/components/panda/core/admin/heading_component.rb +28 -19
- data/app/components/panda/core/admin/page_header_component.rb +107 -0
- data/app/components/panda/core/admin/panel_component.rb +1 -1
- data/app/components/panda/core/admin/slideover_component.rb +92 -4
- data/app/components/panda/core/admin/table_component.rb +11 -11
- data/app/components/panda/core/admin/tag_component.rb +39 -2
- data/app/components/panda/core/admin/user_display_component.rb +4 -5
- data/app/controllers/panda/core/admin/my_profile_controller.rb +10 -3
- data/app/controllers/panda/core/admin/sessions_controller.rb +6 -2
- data/app/controllers/panda/core/admin/test_sessions_controller.rb +60 -0
- data/app/helpers/panda/core/asset_helper.rb +33 -5
- data/app/helpers/panda/core/sessions_helper.rb +26 -1
- 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/image_cropper_controller.js +158 -0
- data/app/javascript/panda/core/controllers/index.js +9 -3
- data/app/javascript/panda/core/controllers/navigation_toggle_controller.js +60 -0
- data/app/javascript/panda/core/controllers/toggle_controller.js +41 -0
- data/app/javascript/panda/core/tailwindplus-elements.js +31 -0
- data/app/models/panda/core/user.rb +60 -6
- data/app/services/panda/core/attach_avatar_service.rb +71 -0
- data/app/views/layouts/panda/core/admin.html.erb +39 -14
- data/app/views/layouts/panda/core/admin_simple.html.erb +1 -0
- data/app/views/panda/core/admin/dashboard/_default_content.html.erb +1 -1
- data/app/views/panda/core/admin/dashboard/show.html.erb +3 -3
- data/app/views/panda/core/admin/my_profile/edit.html.erb +26 -1
- data/app/views/panda/core/admin/sessions/new.html.erb +3 -4
- data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +14 -24
- data/app/views/panda/core/admin/shared/_sidebar.html.erb +69 -14
- data/app/views/panda/core/admin/shared/_slideover.html.erb +1 -1
- data/config/importmap.rb +20 -7
- data/config/routes.rb +10 -1
- data/db/migrate/20250811120000_add_oauth_avatar_url_to_panda_core_users.rb +7 -0
- data/lib/panda/core/asset_loader.rb +5 -2
- data/lib/panda/core/engine.rb +38 -28
- data/lib/panda/core/oauth_providers.rb +3 -3
- data/lib/panda/core/services/base_service.rb +19 -4
- data/lib/panda/core/version.rb +1 -1
- data/lib/panda/core.rb +1 -0
- data/lib/tasks/panda_core_users.rake +158 -0
- metadata +13 -69
- data/lib/generators/panda/core/authentication/templates/reek_spec.rb +0 -43
- data/lib/generators/panda/core/dev_tools/USAGE +0 -24
- data/lib/generators/panda/core/dev_tools/templates/lefthook.yml +0 -13
- data/lib/generators/panda/core/dev_tools/templates/spec_support_panda_core_helpers.rb +0 -18
- data/lib/generators/panda/core/dev_tools_generator.rb +0 -143
- data/lib/generators/panda/core/install_generator.rb +0 -41
- data/lib/generators/panda/core/templates/README +0 -25
- data/lib/generators/panda/core/templates/initializer.rb +0 -44
- data/lib/generators/panda/core/templates_generator.rb +0 -27
- data/lib/panda/core/testing/capybara_config.rb +0 -70
- data/lib/panda/core/testing/omniauth_helpers.rb +0 -52
- data/lib/panda/core/testing/rspec_config.rb +0 -72
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4aa145ee85e5b0c497754d7d218ceb3cc7c4de6065e3357f85e0676186df37f1
|
|
4
|
+
data.tar.gz: 9425c978a3208aca75045e4cf1e36408088829a73c19d10746a91cefc320db6f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef76d583b1a36ecef8d13788338a847457bcd36a8efddf9582dcb1a7fc7ee484cb689f5dea6f9628d1fc46e2e7e2f513c84b6531116059496bb5e2e862d82f32
|
|
7
|
+
data.tar.gz: 982d52f5927bddf30b2c6ab1d3981e78e542fba07717628d73f4375c8819ebf29a151eaf08f0f89fea48d06830c52315d3ff9a996d19472696c061597d529bd4
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
@source "../../../cms/app/components/**/*.rb";
|
|
10
10
|
|
|
11
11
|
@theme {
|
|
12
|
+
/* Legacy colors */
|
|
12
13
|
--color-white: var(--color-white);
|
|
13
14
|
--color-black: var(--color-black);
|
|
14
15
|
--color-light: var(--color-light);
|
|
@@ -19,6 +20,45 @@
|
|
|
19
20
|
--color-inactive: var(--color-inactive);
|
|
20
21
|
--color-warning: var(--color-warning);
|
|
21
22
|
--color-error: var(--color-error);
|
|
23
|
+
|
|
24
|
+
/* Tailwind UI compatible primary scale */
|
|
25
|
+
--color-primary-50: var(--color-primary-50);
|
|
26
|
+
--color-primary-100: var(--color-primary-100);
|
|
27
|
+
--color-primary-200: var(--color-primary-200);
|
|
28
|
+
--color-primary-300: var(--color-primary-300);
|
|
29
|
+
--color-primary-400: var(--color-primary-400);
|
|
30
|
+
--color-primary-500: var(--color-primary-500);
|
|
31
|
+
--color-primary-600: var(--color-primary-600);
|
|
32
|
+
--color-primary-700: var(--color-primary-700);
|
|
33
|
+
--color-primary-800: var(--color-primary-800);
|
|
34
|
+
--color-primary-900: var(--color-primary-900);
|
|
35
|
+
--color-primary-950: var(--color-primary-950);
|
|
36
|
+
|
|
37
|
+
/* Map indigo to primary for Tailwind UI compatibility */
|
|
38
|
+
--color-indigo-50: var(--color-primary-50);
|
|
39
|
+
--color-indigo-100: var(--color-primary-100);
|
|
40
|
+
--color-indigo-200: var(--color-primary-200);
|
|
41
|
+
--color-indigo-300: var(--color-primary-300);
|
|
42
|
+
--color-indigo-400: var(--color-primary-400);
|
|
43
|
+
--color-indigo-500: var(--color-primary-500);
|
|
44
|
+
--color-indigo-600: var(--color-primary-600);
|
|
45
|
+
--color-indigo-700: var(--color-primary-700);
|
|
46
|
+
--color-indigo-800: var(--color-primary-800);
|
|
47
|
+
--color-indigo-900: var(--color-primary-900);
|
|
48
|
+
--color-indigo-950: var(--color-primary-950);
|
|
49
|
+
|
|
50
|
+
/* Gray scale */
|
|
51
|
+
--color-gray-50: var(--color-gray-50);
|
|
52
|
+
--color-gray-100: var(--color-gray-100);
|
|
53
|
+
--color-gray-200: var(--color-gray-200);
|
|
54
|
+
--color-gray-300: var(--color-gray-300);
|
|
55
|
+
--color-gray-400: var(--color-gray-400);
|
|
56
|
+
--color-gray-500: var(--color-gray-500);
|
|
57
|
+
--color-gray-600: var(--color-gray-600);
|
|
58
|
+
--color-gray-700: var(--color-gray-700);
|
|
59
|
+
--color-gray-800: var(--color-gray-800);
|
|
60
|
+
--color-gray-900: var(--color-gray-900);
|
|
61
|
+
--color-gray-950: var(--color-gray-950);
|
|
22
62
|
}
|
|
23
63
|
|
|
24
64
|
@layer base {
|
|
@@ -26,6 +66,7 @@
|
|
|
26
66
|
--color-white: rgb(249, 249, 249); /* #F9F9F9 */
|
|
27
67
|
--color-black: rgb(26, 22, 29); /* #1A161D */
|
|
28
68
|
|
|
69
|
+
/* Legacy color variables (kept for backwards compatibility) */
|
|
29
70
|
--color-light: rgb(238, 206, 230); /* #EECEE6 */
|
|
30
71
|
--color-mid: rgb(141, 94, 183); /* #8D5EB7 */
|
|
31
72
|
--color-dark: rgb(33, 29, 73); /* #211D49 */
|
|
@@ -36,11 +77,39 @@
|
|
|
36
77
|
--color-warning: rgb(250, 207, 142); /* #FACF8E */
|
|
37
78
|
--color-inactive: rgb(216, 247, 245); /* #d6e4f7 */
|
|
38
79
|
--color-error: rgb(245, 129, 129); /* #F58181 */
|
|
80
|
+
|
|
81
|
+
/* Full Tailwind UI compatible color scale - Primary (Purple palette) */
|
|
82
|
+
--color-primary-50: rgb(250, 245, 255); /* Very light purple */
|
|
83
|
+
--color-primary-100: rgb(243, 232, 255); /* Lighter purple */
|
|
84
|
+
--color-primary-200: rgb(233, 213, 255); /* Light purple */
|
|
85
|
+
--color-primary-300: rgb(216, 180, 254); /* Medium light purple */
|
|
86
|
+
--color-primary-400: rgb(192, 132, 252); /* Medium purple */
|
|
87
|
+
--color-primary-500: rgb(141, 94, 183); /* Main brand color (mid) */
|
|
88
|
+
--color-primary-600: rgb(120, 80, 160); /* Darker purple */
|
|
89
|
+
--color-primary-700: rgb(100, 65, 140); /* Dark purple */
|
|
90
|
+
--color-primary-800: rgb(60, 45, 100); /* Very dark purple */
|
|
91
|
+
--color-primary-900: rgb(33, 29, 73); /* Darkest (dark) */
|
|
92
|
+
--color-primary-950: rgb(24, 20, 50); /* Ultra dark */
|
|
93
|
+
|
|
94
|
+
/* Gray scale for UI elements */
|
|
95
|
+
--color-gray-50: rgb(249, 250, 251);
|
|
96
|
+
--color-gray-100: rgb(243, 244, 246);
|
|
97
|
+
--color-gray-200: rgb(229, 231, 235);
|
|
98
|
+
--color-gray-300: rgb(209, 213, 219);
|
|
99
|
+
--color-gray-400: rgb(156, 163, 175);
|
|
100
|
+
--color-gray-500: rgb(107, 114, 128);
|
|
101
|
+
--color-gray-600: rgb(75, 85, 99);
|
|
102
|
+
--color-gray-700: rgb(55, 65, 81);
|
|
103
|
+
--color-gray-800: rgb(31, 41, 55);
|
|
104
|
+
--color-gray-900: rgb(17, 24, 39);
|
|
105
|
+
--color-gray-950: rgb(3, 7, 18);
|
|
39
106
|
}
|
|
40
107
|
|
|
41
108
|
html[data-theme='sky'] {
|
|
42
109
|
--color-white: rgb(249, 249, 249); /* #F9F9F9 */
|
|
43
110
|
--color-black: rgb(26, 22, 29); /* #1A161D */
|
|
111
|
+
|
|
112
|
+
/* Legacy color variables (kept for backwards compatibility) */
|
|
44
113
|
--color-light: rgb(204, 238, 242); /* #CCEEF2 */
|
|
45
114
|
--color-mid: rgb(42, 102, 159); /* #2A669F */
|
|
46
115
|
--color-dark: rgb(20, 32, 74); /* #14204A */
|
|
@@ -50,6 +119,32 @@
|
|
|
50
119
|
--color-warning: rgb(244, 190, 102); /* #F4BE66 */
|
|
51
120
|
--color-inactive: rgb(216, 247, 245); /* #d6e4f7 */
|
|
52
121
|
--color-error: rgb(208, 64, 20); /* #D04014 */
|
|
122
|
+
|
|
123
|
+
/* Full Tailwind UI compatible color scale - Primary (Blue palette) */
|
|
124
|
+
--color-primary-50: rgb(240, 249, 255); /* Very light blue */
|
|
125
|
+
--color-primary-100: rgb(224, 242, 254); /* Lighter blue */
|
|
126
|
+
--color-primary-200: rgb(186, 230, 253); /* Light blue */
|
|
127
|
+
--color-primary-300: rgb(125, 211, 252); /* Medium light blue */
|
|
128
|
+
--color-primary-400: rgb(56, 189, 248); /* Medium blue */
|
|
129
|
+
--color-primary-500: rgb(42, 102, 159); /* Main brand color (mid) */
|
|
130
|
+
--color-primary-600: rgb(35, 85, 132); /* Darker blue */
|
|
131
|
+
--color-primary-700: rgb(28, 68, 106); /* Dark blue */
|
|
132
|
+
--color-primary-800: rgb(22, 52, 82); /* Very dark blue */
|
|
133
|
+
--color-primary-900: rgb(20, 32, 74); /* Darkest (dark) */
|
|
134
|
+
--color-primary-950: rgb(15, 24, 55); /* Ultra dark */
|
|
135
|
+
|
|
136
|
+
/* Gray scale for UI elements (same across themes) */
|
|
137
|
+
--color-gray-50: rgb(249, 250, 251);
|
|
138
|
+
--color-gray-100: rgb(243, 244, 246);
|
|
139
|
+
--color-gray-200: rgb(229, 231, 235);
|
|
140
|
+
--color-gray-300: rgb(209, 213, 219);
|
|
141
|
+
--color-gray-400: rgb(156, 163, 175);
|
|
142
|
+
--color-gray-500: rgb(107, 114, 128);
|
|
143
|
+
--color-gray-600: rgb(75, 85, 99);
|
|
144
|
+
--color-gray-700: rgb(55, 65, 81);
|
|
145
|
+
--color-gray-800: rgb(31, 41, 55);
|
|
146
|
+
--color-gray-900: rgb(17, 24, 39);
|
|
147
|
+
--color-gray-950: rgb(3, 7, 18);
|
|
53
148
|
}
|
|
54
149
|
|
|
55
150
|
a.block-link:after {
|
|
@@ -11,6 +11,13 @@ module Panda
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def text_field(attribute, options = {})
|
|
14
|
+
# Add disabled/readonly styling
|
|
15
|
+
field_classes = if options[:readonly] || options[:disabled]
|
|
16
|
+
readonly_input_styles
|
|
17
|
+
else
|
|
18
|
+
input_styles
|
|
19
|
+
end
|
|
20
|
+
|
|
14
21
|
if options.dig(:data, :prefix)
|
|
15
22
|
content_tag :div, class: container_styles do
|
|
16
23
|
label(attribute) + meta_text(options) +
|
|
@@ -19,12 +26,12 @@ module Panda
|
|
|
19
26
|
class: "inline-flex items-center px-3 text-base border border-r-none rounded-s-md whitespace-nowrap break-keep") do
|
|
20
27
|
options.dig(:data, :prefix)
|
|
21
28
|
end +
|
|
22
|
-
super(attribute, options.reverse_merge(class: "#{
|
|
29
|
+
super(attribute, options.reverse_merge(class: "#{field_classes} input-prefix rounded-l-none border-l-none"))
|
|
23
30
|
end + error_message(attribute)
|
|
24
31
|
end
|
|
25
32
|
else
|
|
26
33
|
content_tag :div, class: container_styles do
|
|
27
|
-
label(attribute) + meta_text(options) + super(attribute, options.reverse_merge(class:
|
|
34
|
+
label(attribute) + meta_text(options) + super(attribute, options.reverse_merge(class: field_classes)) + error_message(attribute)
|
|
28
35
|
end
|
|
29
36
|
end
|
|
30
37
|
end
|
|
@@ -77,8 +84,121 @@ module Panda
|
|
|
77
84
|
end
|
|
78
85
|
|
|
79
86
|
def file_field(method, options = {})
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
# Check if cropper is requested
|
|
88
|
+
with_cropper = options.delete(:with_cropper)
|
|
89
|
+
|
|
90
|
+
# Check if simple mode is requested (no fancy upload UI)
|
|
91
|
+
simple_mode = options.delete(:simple)
|
|
92
|
+
|
|
93
|
+
if with_cropper
|
|
94
|
+
# Image upload with cropper
|
|
95
|
+
aspect_ratio = options.delete(:aspect_ratio) # e.g., 1.91 for OG images (1200x630)
|
|
96
|
+
min_width = options.delete(:min_width) || 0
|
|
97
|
+
min_height = options.delete(:min_height) || 0
|
|
98
|
+
accept_types = options.delete(:accept) || "image/*"
|
|
99
|
+
field_id = "#{object_name}_#{method}"
|
|
100
|
+
|
|
101
|
+
content_tag :div, class: container_styles do
|
|
102
|
+
label(method) +
|
|
103
|
+
meta_text(options) +
|
|
104
|
+
# Cropper stylesheet
|
|
105
|
+
@template.content_tag(:link, nil, rel: "stylesheet", href: "https://cdn.jsdelivr.net/npm/cropperjs@1.6.2/dist/cropper.min.css") +
|
|
106
|
+
# File input
|
|
107
|
+
content_tag(:div, class: "mt-2") do
|
|
108
|
+
super(method, options.reverse_merge(
|
|
109
|
+
id: field_id,
|
|
110
|
+
accept: accept_types,
|
|
111
|
+
class: "file:rounded file:border-0 file:text-sm file:bg-white file:text-gray-500 hover:file:bg-gray-50 bg-white px-2.5 hover:bg-gray-50 #{input_styles}",
|
|
112
|
+
data: {
|
|
113
|
+
controller: "image-cropper",
|
|
114
|
+
image_cropper_target: "input",
|
|
115
|
+
action: "change->image-cropper#handleFileSelect",
|
|
116
|
+
image_cropper_aspect_ratio_value: aspect_ratio,
|
|
117
|
+
image_cropper_min_width_value: min_width,
|
|
118
|
+
image_cropper_min_height_value: min_height
|
|
119
|
+
}
|
|
120
|
+
))
|
|
121
|
+
end +
|
|
122
|
+
# Cropper container (hidden by default)
|
|
123
|
+
content_tag(:div, class: "hidden mt-4 bg-gray-100 dark:bg-gray-800 p-4 rounded-lg", data: {image_cropper_target: "cropperContainer"}) do
|
|
124
|
+
# Preview image
|
|
125
|
+
@template.image_tag("", alt: "Crop preview", data: {image_cropper_target: "preview"}, class: "max-w-full") +
|
|
126
|
+
# Cropper controls
|
|
127
|
+
content_tag(:div, class: "mt-4 flex gap-2 flex-wrap") do
|
|
128
|
+
@template.button_tag("Crop & Save", type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500", data: {action: "click->image-cropper#crop"}) +
|
|
129
|
+
@template.button_tag("Cancel", type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50", data: {action: "click->image-cropper#cancel"}) +
|
|
130
|
+
@template.button_tag(type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50", data: {action: "click->image-cropper#reset"}) do
|
|
131
|
+
@template.content_tag(:i, "", class: "fa-solid fa-rotate-left") +
|
|
132
|
+
@template.content_tag(:span, "Reset")
|
|
133
|
+
end +
|
|
134
|
+
@template.button_tag(type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50", data: {action: "click->image-cropper#rotate", degrees: "90"}) do
|
|
135
|
+
@template.content_tag(:i, "", class: "fa-solid fa-rotate-right") +
|
|
136
|
+
@template.content_tag(:span, "Rotate")
|
|
137
|
+
end +
|
|
138
|
+
@template.button_tag(type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50", data: {action: "click->image-cropper#flip", direction: "horizontal"}) do
|
|
139
|
+
@template.content_tag(:i, "", class: "fa-solid fa-arrows-left-right") +
|
|
140
|
+
@template.content_tag(:span, "Flip H")
|
|
141
|
+
end +
|
|
142
|
+
@template.button_tag(type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50", data: {action: "click->image-cropper#zoom", ratio: "0.1"}) do
|
|
143
|
+
@template.content_tag(:i, "", class: "fa-solid fa-magnifying-glass-plus") +
|
|
144
|
+
@template.content_tag(:span, "Zoom In")
|
|
145
|
+
end +
|
|
146
|
+
@template.button_tag(type: "button", class: "inline-flex items-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50", data: {action: "click->image-cropper#zoom", ratio: "-0.1"}) do
|
|
147
|
+
@template.content_tag(:i, "", class: "fa-solid fa-magnifying-glass-minus") +
|
|
148
|
+
@template.content_tag(:span, "Zoom Out")
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
elsif simple_mode
|
|
154
|
+
# Simple file input with basic styling
|
|
155
|
+
content_tag :div, class: container_styles do
|
|
156
|
+
label(method) +
|
|
157
|
+
meta_text(options) +
|
|
158
|
+
super(method, options.reverse_merge(class: "file:rounded file:border-0 file:text-sm file:bg-white file:text-gray-500 hover:file:bg-gray-50 bg-white px-2.5 hover:bg-gray-50".concat(input_styles)))
|
|
159
|
+
end
|
|
160
|
+
else
|
|
161
|
+
# Fancy drag-and-drop UI
|
|
162
|
+
accept_types = options.delete(:accept) || "image/*"
|
|
163
|
+
max_size = options.delete(:max_size) || "10MB"
|
|
164
|
+
file_types_display = options.delete(:file_types_display) || "PNG, JPG, GIF"
|
|
165
|
+
|
|
166
|
+
field_id = "#{object_name}_#{method}"
|
|
167
|
+
|
|
168
|
+
content_tag :div, class: "#{container_styles} col-span-full", data: {controller: "file-upload"} do
|
|
169
|
+
label(method) +
|
|
170
|
+
meta_text(options) +
|
|
171
|
+
content_tag(:div, class: "mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 px-6 py-10 dark:border-white/25 transition-colors", data: {file_upload_target: "dropzone"}) do
|
|
172
|
+
content_tag(:div, class: "text-center") do
|
|
173
|
+
# Icon
|
|
174
|
+
@template.content_tag(:svg, viewBox: "0 0 24 24", fill: "currentColor", "data-slot": "icon", "aria-hidden": true, class: "mx-auto size-12 text-gray-300 dark:text-gray-600") do
|
|
175
|
+
@template.content_tag(:path, nil, d: "M1.5 6a2.25 2.25 0 0 1 2.25-2.25h16.5A2.25 2.25 0 0 1 22.5 6v12a2.25 2.25 0 0 1-2.25 2.25H3.75A2.25 2.25 0 0 1 1.5 18V6ZM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0 0 21 18v-1.94l-2.69-2.689a1.5 1.5 0 0 0-2.12 0l-.88.879.97.97a.75.75 0 1 1-1.06 1.06l-5.16-5.159a1.5 1.5 0 0 0-2.12 0L3 16.061Zm10.125-7.81a1.125 1.125 0 1 1 2.25 0 1.125 1.125 0 0 1-2.25 0Z", "clip-rule": "evenodd", "fill-rule": "evenodd")
|
|
176
|
+
end +
|
|
177
|
+
# Upload area
|
|
178
|
+
content_tag(:div, class: "mt-4 flex items-baseline justify-center text-sm leading-6 text-gray-600 dark:text-gray-400") do
|
|
179
|
+
content_tag(:label, for: field_id, class: "relative cursor-pointer rounded-md bg-transparent font-semibold text-indigo-600 focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:focus-within:outline-indigo-500 dark:hover:text-indigo-300") do
|
|
180
|
+
content_tag(:span, "Upload a file") +
|
|
181
|
+
super(method, options.reverse_merge(
|
|
182
|
+
id: field_id,
|
|
183
|
+
accept: accept_types,
|
|
184
|
+
class: "sr-only",
|
|
185
|
+
data: {
|
|
186
|
+
file_upload_target: "input",
|
|
187
|
+
action: "change->file-upload#handleFileSelect"
|
|
188
|
+
}
|
|
189
|
+
))
|
|
190
|
+
end +
|
|
191
|
+
content_tag(:span, "or drag and drop", class: "pl-1")
|
|
192
|
+
end +
|
|
193
|
+
# File type info
|
|
194
|
+
content_tag(:p, "#{file_types_display} up to #{max_size}", class: "text-xs/5 text-gray-600 dark:text-gray-400")
|
|
195
|
+
end
|
|
196
|
+
end +
|
|
197
|
+
# File info display (hidden by default)
|
|
198
|
+
content_tag(:div, "", class: "hidden mt-3", data: {file_upload_target: "fileInfo"}) +
|
|
199
|
+
# Preview display (hidden by default)
|
|
200
|
+
content_tag(:div, "", class: "hidden mt-3", data: {file_upload_target: "preview"})
|
|
201
|
+
end
|
|
82
202
|
end
|
|
83
203
|
end
|
|
84
204
|
|
|
@@ -117,11 +237,11 @@ module Panda
|
|
|
117
237
|
def submit(value = nil, options = {})
|
|
118
238
|
value ||= submit_default_value
|
|
119
239
|
|
|
120
|
-
# Use the
|
|
240
|
+
# Use the primary mid color for save/create actions
|
|
121
241
|
action = object.persisted? ? :save : :create
|
|
122
242
|
button_classes = case action
|
|
123
243
|
when :save, :create
|
|
124
|
-
"text-white bg-
|
|
244
|
+
"text-white bg-mid hover:bg-mid/80"
|
|
125
245
|
when :save_inactive
|
|
126
246
|
"text-white bg-gray-400"
|
|
127
247
|
when :secondary
|
|
@@ -150,32 +270,64 @@ module Panda
|
|
|
150
270
|
end
|
|
151
271
|
end
|
|
152
272
|
|
|
273
|
+
def radio_button_group(method, choices, options = {})
|
|
274
|
+
current_value = object.send(method)
|
|
275
|
+
|
|
276
|
+
content_tag :div, class: container_styles do
|
|
277
|
+
label(method) +
|
|
278
|
+
meta_text(options) +
|
|
279
|
+
content_tag(:div, class: "mt-2 space-y-2") do
|
|
280
|
+
choices.map do |choice|
|
|
281
|
+
choice_value = choice.is_a?(Array) ? choice.last : choice
|
|
282
|
+
choice_label = choice.is_a?(Array) ? choice.first : choice.to_s.humanize
|
|
283
|
+
choice_id = "#{object_name}_#{method}_#{choice_value}"
|
|
284
|
+
is_checked = (current_value.to_s == choice_value.to_s)
|
|
285
|
+
|
|
286
|
+
content_tag(:label, class: "flex items-center gap-x-3 rounded-lg border border-gray-300 px-3 py-3 text-sm/6 font-medium cursor-pointer hover:bg-gray-50 dark:border-white/10 dark:hover:bg-white/5") do
|
|
287
|
+
radio_button(method, choice_value, {id: choice_id, checked: is_checked, class: "size-4 border-gray-300 text-indigo-600 focus:ring-indigo-600 dark:border-white/10 dark:bg-white/5"}) +
|
|
288
|
+
content_tag(:span, choice_label, class: "text-gray-900 dark:text-white")
|
|
289
|
+
end
|
|
290
|
+
end.join.html_safe
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
153
295
|
def meta_text(options)
|
|
154
296
|
return unless options[:meta]
|
|
155
297
|
|
|
156
298
|
@template.content_tag(:p, options[:meta], class: "block text-black/60 text-sm mb-2")
|
|
157
299
|
end
|
|
158
300
|
|
|
301
|
+
def section_heading(text, options = {})
|
|
302
|
+
@template.content_tag(:div, class: "-mx-4 sm:-mx-6 px-4 sm:px-6 py-4 bg-gray-200 dark:bg-gray-700 mb-6") do
|
|
303
|
+
@template.content_tag(:h3, text, class: "text-base font-semibold text-gray-900 dark:text-white")
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
159
307
|
private
|
|
160
308
|
|
|
161
309
|
def label_styles
|
|
162
|
-
"
|
|
310
|
+
"block text-sm/6 font-medium text-gray-900 dark:text-gray-100"
|
|
163
311
|
end
|
|
164
312
|
|
|
165
313
|
def base_input_styles
|
|
166
|
-
"
|
|
314
|
+
"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500"
|
|
167
315
|
end
|
|
168
316
|
|
|
169
317
|
def input_styles
|
|
170
318
|
base_input_styles
|
|
171
319
|
end
|
|
172
320
|
|
|
321
|
+
def readonly_input_styles
|
|
322
|
+
"block w-full rounded-md bg-gray-50 px-3 py-1.5 text-base text-gray-500 outline-1 -outline-offset-1 outline-gray-200 cursor-not-allowed sm:text-sm/6 dark:bg-white/10 dark:text-gray-500 dark:outline-white/5"
|
|
323
|
+
end
|
|
324
|
+
|
|
173
325
|
def input_styles_prefix
|
|
174
|
-
input_styles
|
|
326
|
+
"#{input_styles} prefix"
|
|
175
327
|
end
|
|
176
328
|
|
|
177
329
|
def select_styles
|
|
178
|
-
"
|
|
330
|
+
"block w-full rounded-md bg-white px-3 py-1.5 pr-8 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 appearance-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:focus:outline-indigo-500"
|
|
179
331
|
end
|
|
180
332
|
|
|
181
333
|
def select_svg
|
|
@@ -194,7 +346,7 @@ module Panda
|
|
|
194
346
|
end
|
|
195
347
|
|
|
196
348
|
def textarea_styles
|
|
197
|
-
input_styles
|
|
349
|
+
"#{input_styles} min-h-32"
|
|
198
350
|
end
|
|
199
351
|
|
|
200
352
|
def submit_default_value
|
|
@@ -5,17 +5,24 @@ module Panda
|
|
|
5
5
|
module UI
|
|
6
6
|
# Modern Phlex-based button component with type-safe props.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# Supports both <button> and <a> elements based on whether an href is provided.
|
|
9
|
+
# Follows Tailwind UI Plus styling patterns with dark mode support.
|
|
10
10
|
#
|
|
11
|
-
# @example Basic
|
|
11
|
+
# @example Basic button
|
|
12
12
|
# render Panda::Core::UI::Button.new(text: "Click me")
|
|
13
13
|
#
|
|
14
|
-
# @example
|
|
14
|
+
# @example Button as link
|
|
15
15
|
# render Panda::Core::UI::Button.new(
|
|
16
|
-
# text: "
|
|
17
|
-
# variant: :
|
|
18
|
-
#
|
|
16
|
+
# text: "Edit",
|
|
17
|
+
# variant: :secondary,
|
|
18
|
+
# href: "/admin/posts/1/edit"
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
# @example Primary action button
|
|
22
|
+
# render Panda::Core::UI::Button.new(
|
|
23
|
+
# text: "Publish",
|
|
24
|
+
# variant: :primary,
|
|
25
|
+
# href: "/admin/posts/1/publish"
|
|
19
26
|
# )
|
|
20
27
|
#
|
|
21
28
|
# @example With custom attributes
|
|
@@ -33,54 +40,68 @@ module Panda
|
|
|
33
40
|
prop :size, Symbol, default: :medium
|
|
34
41
|
prop :disabled, _Boolean, default: false
|
|
35
42
|
prop :type, String, default: "button"
|
|
43
|
+
prop :href, _Nilable(String), default: -> {}
|
|
36
44
|
|
|
37
45
|
def view_template
|
|
38
|
-
|
|
46
|
+
if @href
|
|
47
|
+
a(**@attrs) { @text }
|
|
48
|
+
else
|
|
49
|
+
button(**@attrs) { @text }
|
|
50
|
+
end
|
|
39
51
|
end
|
|
40
52
|
|
|
41
53
|
def default_attrs
|
|
42
|
-
{
|
|
43
|
-
type: type,
|
|
44
|
-
disabled: disabled,
|
|
54
|
+
base = {
|
|
45
55
|
class: button_classes
|
|
46
56
|
}
|
|
57
|
+
|
|
58
|
+
if @href
|
|
59
|
+
base[:href] = @href
|
|
60
|
+
else
|
|
61
|
+
base[:type] = @type
|
|
62
|
+
base[:disabled] = @disabled if @disabled
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
base
|
|
47
66
|
end
|
|
48
67
|
|
|
49
68
|
private
|
|
50
69
|
|
|
51
70
|
def button_classes
|
|
52
|
-
base = "inline-flex items-center rounded-md font-
|
|
71
|
+
base = "inline-flex items-center rounded-md font-semibold"
|
|
53
72
|
base += " #{size_classes}"
|
|
54
73
|
base += " #{variant_classes}"
|
|
55
|
-
base += " disabled:opacity-50 disabled:cursor-not-allowed" if disabled
|
|
74
|
+
base += " disabled:opacity-50 disabled:cursor-not-allowed" if @disabled
|
|
56
75
|
base
|
|
57
76
|
end
|
|
58
77
|
|
|
59
78
|
def size_classes
|
|
60
|
-
case size
|
|
79
|
+
case @size
|
|
61
80
|
when :small, :sm
|
|
62
|
-
"
|
|
81
|
+
"px-2.5 py-1.5 text-sm"
|
|
63
82
|
when :large, :lg
|
|
64
|
-
"
|
|
83
|
+
"px-3.5 py-2.5 text-lg"
|
|
65
84
|
else # :medium, :md
|
|
66
|
-
"
|
|
85
|
+
"px-3 py-2 text-sm"
|
|
67
86
|
end
|
|
68
87
|
end
|
|
69
88
|
|
|
70
89
|
def variant_classes
|
|
71
|
-
case variant
|
|
90
|
+
case @variant
|
|
72
91
|
when :primary
|
|
73
|
-
|
|
92
|
+
# Blue primary button with dark mode support
|
|
93
|
+
"bg-blue-600 text-white shadow-xs hover:bg-blue-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:bg-blue-500 dark:shadow-none dark:hover:bg-blue-400 dark:focus-visible:outline-blue-500"
|
|
74
94
|
when :secondary
|
|
75
|
-
|
|
95
|
+
# White/gray secondary button with ring and dark mode support
|
|
96
|
+
"bg-white text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20"
|
|
76
97
|
when :success
|
|
77
|
-
"bg-green-600 text-white hover:bg-green-
|
|
98
|
+
"bg-green-600 text-white shadow-xs hover:bg-green-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600 dark:bg-green-500 dark:shadow-none dark:hover:bg-green-400 dark:focus-visible:outline-green-500"
|
|
78
99
|
when :danger
|
|
79
|
-
"bg-red-600 text-white hover:bg-red-
|
|
100
|
+
"bg-red-600 text-white shadow-xs hover:bg-red-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 dark:bg-red-500 dark:shadow-none dark:hover:bg-red-400 dark:focus-visible:outline-red-500"
|
|
80
101
|
when :ghost
|
|
81
|
-
"bg-transparent text-gray-700 hover:bg-gray-100"
|
|
102
|
+
"bg-transparent text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
|
82
103
|
else # :default
|
|
83
|
-
"bg-gray-700 text-white hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-
|
|
104
|
+
"bg-gray-700 text-white shadow-xs hover:bg-gray-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-700 dark:bg-gray-600 dark:shadow-none dark:hover:bg-gray-500 dark:focus-visible:outline-gray-600"
|
|
84
105
|
end
|
|
85
106
|
end
|
|
86
107
|
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
# Breadcrumb navigation component with responsive behavior.
|
|
7
|
+
#
|
|
8
|
+
# Shows a "Back" link on mobile and full breadcrumb trail on larger screens.
|
|
9
|
+
# Follows Tailwind UI Plus pattern for breadcrumb navigation.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic breadcrumbs
|
|
12
|
+
# render Panda::Core::Admin::BreadcrumbComponent.new(
|
|
13
|
+
# items: [
|
|
14
|
+
# { text: "Pages", href: "/admin/pages" },
|
|
15
|
+
# { text: "Blog Posts", href: "/admin/pages/blog" },
|
|
16
|
+
# { text: "Edit Post", href: "/admin/pages/blog/1/edit" }
|
|
17
|
+
# ]
|
|
18
|
+
# )
|
|
19
|
+
#
|
|
20
|
+
# @example Without back link (first page in section)
|
|
21
|
+
# render Panda::Core::Admin::BreadcrumbComponent.new(
|
|
22
|
+
# items: [
|
|
23
|
+
# { text: "Dashboard", href: "/admin" }
|
|
24
|
+
# ],
|
|
25
|
+
# show_back: false
|
|
26
|
+
# )
|
|
27
|
+
#
|
|
28
|
+
class BreadcrumbComponent < Panda::Core::Base
|
|
29
|
+
prop :items, Array, default: -> { [] }
|
|
30
|
+
prop :show_back, _Boolean, default: true
|
|
31
|
+
|
|
32
|
+
def view_template
|
|
33
|
+
div do
|
|
34
|
+
# Mobile back link
|
|
35
|
+
if @show_back && @items.any?
|
|
36
|
+
nav(
|
|
37
|
+
aria: {label: "Back"},
|
|
38
|
+
class: "sm:hidden"
|
|
39
|
+
) do
|
|
40
|
+
a(
|
|
41
|
+
href: back_link_href,
|
|
42
|
+
class: "flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
|
43
|
+
) do
|
|
44
|
+
render_chevron_left_icon
|
|
45
|
+
plain "Back"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Desktop breadcrumb trail
|
|
51
|
+
nav(
|
|
52
|
+
aria: {label: "Breadcrumb"},
|
|
53
|
+
class: "hidden sm:flex"
|
|
54
|
+
) do
|
|
55
|
+
ol(
|
|
56
|
+
role: "list",
|
|
57
|
+
class: "flex items-center space-x-4"
|
|
58
|
+
) do
|
|
59
|
+
@items.each_with_index do |item, index|
|
|
60
|
+
li do
|
|
61
|
+
if index.zero?
|
|
62
|
+
# First item (no separator)
|
|
63
|
+
div(class: "flex") do
|
|
64
|
+
a(
|
|
65
|
+
href: item[:href],
|
|
66
|
+
class: breadcrumb_link_classes(index)
|
|
67
|
+
) { item[:text] }
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
# Subsequent items (with separator)
|
|
71
|
+
div(class: "flex items-center") do
|
|
72
|
+
render_chevron_right_icon
|
|
73
|
+
a(
|
|
74
|
+
href: item[:href],
|
|
75
|
+
aria: ((index == @items.length - 1) ? {current: "page"} : nil),
|
|
76
|
+
class: breadcrumb_link_classes(index)
|
|
77
|
+
) { item[:text] }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def back_link_href
|
|
90
|
+
(@items.length > 1) ? @items[-2][:href] : @items.first[:href]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def breadcrumb_link_classes(index)
|
|
94
|
+
base = "text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
|
95
|
+
base += " ml-4" unless index.zero?
|
|
96
|
+
base
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def render_chevron_left_icon
|
|
100
|
+
svg(
|
|
101
|
+
viewBox: "0 0 20 20",
|
|
102
|
+
fill: "currentColor",
|
|
103
|
+
data: {slot: "icon"},
|
|
104
|
+
aria: {hidden: "true"},
|
|
105
|
+
class: "mr-1 -ml-1 size-5 shrink-0 text-gray-400 dark:text-gray-500"
|
|
106
|
+
) do |s|
|
|
107
|
+
s.path(
|
|
108
|
+
d: "M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z",
|
|
109
|
+
clip_rule: "evenodd",
|
|
110
|
+
fill_rule: "evenodd"
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def render_chevron_right_icon
|
|
116
|
+
svg(
|
|
117
|
+
viewBox: "0 0 20 20",
|
|
118
|
+
fill: "currentColor",
|
|
119
|
+
data: {slot: "icon"},
|
|
120
|
+
aria: {hidden: "true"},
|
|
121
|
+
class: "size-5 shrink-0 text-gray-400 dark:text-gray-500"
|
|
122
|
+
) do |s|
|
|
123
|
+
s.path(
|
|
124
|
+
d: "M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z",
|
|
125
|
+
clip_rule: "evenodd",
|
|
126
|
+
fill_rule: "evenodd"
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|