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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aafa859a0f30c0eac8666376380b18608a5429f760a1f30f2db283504458533d
|
|
4
|
+
data.tar.gz: 8e63ccaa5fd464c9a7e403a6fa2d3f789bfb0106ce824c31e59f30ffc0bc3dae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8508b00f4c356dfceb223952d34dec3d38eec94078043ab40f08ebcdf2f1e484c2eefbe8534f3c1b86de5b62b71b0cc62dc0c63168e6053f68cf7ed9bf879198
|
|
7
|
+
data.tar.gz: 8c2c61ff18769f7ee23defaaae9f6d24703328a7e8269079af53508a811ce71dedd8948a0c0906f5927ec8f66e65e04319f4649541e3b928291712f49559088c
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
@import
|
|
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 {
|
|
12
|
+
/* Legacy colors */
|
|
4
13
|
--color-white: var(--color-white);
|
|
5
14
|
--color-black: var(--color-black);
|
|
6
15
|
--color-light: var(--color-light);
|
|
@@ -11,13 +20,53 @@
|
|
|
11
20
|
--color-inactive: var(--color-inactive);
|
|
12
21
|
--color-warning: var(--color-warning);
|
|
13
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);
|
|
14
62
|
}
|
|
15
63
|
|
|
16
64
|
@layer base {
|
|
17
|
-
html[data-theme=
|
|
65
|
+
html[data-theme='default'] {
|
|
18
66
|
--color-white: rgb(249, 249, 249); /* #F9F9F9 */
|
|
19
67
|
--color-black: rgb(26, 22, 29); /* #1A161D */
|
|
20
68
|
|
|
69
|
+
/* Legacy color variables (kept for backwards compatibility) */
|
|
21
70
|
--color-light: rgb(238, 206, 230); /* #EECEE6 */
|
|
22
71
|
--color-mid: rgb(141, 94, 183); /* #8D5EB7 */
|
|
23
72
|
--color-dark: rgb(33, 29, 73); /* #211D49 */
|
|
@@ -28,11 +77,39 @@
|
|
|
28
77
|
--color-warning: rgb(250, 207, 142); /* #FACF8E */
|
|
29
78
|
--color-inactive: rgb(216, 247, 245); /* #d6e4f7 */
|
|
30
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);
|
|
31
106
|
}
|
|
32
107
|
|
|
33
|
-
html[data-theme=
|
|
108
|
+
html[data-theme='sky'] {
|
|
34
109
|
--color-white: rgb(249, 249, 249); /* #F9F9F9 */
|
|
35
110
|
--color-black: rgb(26, 22, 29); /* #1A161D */
|
|
111
|
+
|
|
112
|
+
/* Legacy color variables (kept for backwards compatibility) */
|
|
36
113
|
--color-light: rgb(204, 238, 242); /* #CCEEF2 */
|
|
37
114
|
--color-mid: rgb(42, 102, 159); /* #2A669F */
|
|
38
115
|
--color-dark: rgb(20, 32, 74); /* #14204A */
|
|
@@ -42,24 +119,139 @@
|
|
|
42
119
|
--color-warning: rgb(244, 190, 102); /* #F4BE66 */
|
|
43
120
|
--color-inactive: rgb(216, 247, 245); /* #d6e4f7 */
|
|
44
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);
|
|
45
148
|
}
|
|
46
149
|
|
|
47
150
|
a.block-link:after {
|
|
48
151
|
position: absolute;
|
|
49
|
-
content:
|
|
152
|
+
content: '';
|
|
50
153
|
inset: 0;
|
|
51
154
|
}
|
|
52
155
|
|
|
53
156
|
/* Admin gradient backgrounds */
|
|
54
|
-
html[data-theme=
|
|
157
|
+
html[data-theme='default'] .bg-gradient-admin {
|
|
55
158
|
background: linear-gradient(to bottom right, rgb(33, 29, 73), rgb(141, 94, 183));
|
|
56
159
|
}
|
|
57
160
|
|
|
58
|
-
html[data-theme=
|
|
161
|
+
html[data-theme='sky'] .bg-gradient-admin {
|
|
59
162
|
background: linear-gradient(to bottom right, rgb(20, 32, 74), rgb(42, 102, 159));
|
|
60
163
|
}
|
|
61
164
|
}
|
|
62
165
|
|
|
166
|
+
/* Form input styles */
|
|
167
|
+
@layer components {
|
|
168
|
+
/* Base form field styles - matches panda-cms admin pattern */
|
|
169
|
+
input[type='text'],
|
|
170
|
+
input[type='email'],
|
|
171
|
+
input[type='password'],
|
|
172
|
+
input[type='url'],
|
|
173
|
+
input[type='tel'],
|
|
174
|
+
input[type='number'],
|
|
175
|
+
input[type='date'],
|
|
176
|
+
input[type='datetime-local'],
|
|
177
|
+
input[type='month'],
|
|
178
|
+
input[type='week'],
|
|
179
|
+
input[type='time'],
|
|
180
|
+
input[type='search'],
|
|
181
|
+
textarea {
|
|
182
|
+
@apply block w-full rounded-md border-0 p-2 text-gray-900 bg-white;
|
|
183
|
+
@apply ring-1 ring-inset ring-mid placeholder:text-gray-300;
|
|
184
|
+
@apply focus:ring-1 focus:ring-inset focus:ring-dark;
|
|
185
|
+
@apply hover:cursor-pointer sm:leading-6;
|
|
186
|
+
@apply disabled:ring-gray-300 disabled:focus:ring-gray-300 disabled:bg-gray-50 disabled:cursor-not-allowed;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* Select specific styling - matches panda-cms admin pattern */
|
|
190
|
+
select {
|
|
191
|
+
@apply block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 bg-white;
|
|
192
|
+
@apply ring-1 ring-inset ring-mid;
|
|
193
|
+
@apply focus:ring-1 focus:ring-inset focus:ring-dark;
|
|
194
|
+
@apply hover:cursor-pointer sm:leading-6;
|
|
195
|
+
@apply disabled:ring-gray-300 disabled:focus:ring-gray-300 disabled:bg-gray-50 disabled:cursor-not-allowed;
|
|
196
|
+
@apply appearance-none bg-right bg-no-repeat;
|
|
197
|
+
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");
|
|
198
|
+
background-position: right 0.5rem center;
|
|
199
|
+
background-size: 1.5em 1.5em;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Checkbox and radio styling */
|
|
203
|
+
input[type='checkbox'],
|
|
204
|
+
input[type='radio'] {
|
|
205
|
+
@apply w-4 h-4 text-indigo-600 bg-white border-gray-300 rounded transition-colors duration-200;
|
|
206
|
+
@apply focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2;
|
|
207
|
+
@apply disabled:bg-gray-100 disabled:cursor-not-allowed;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
input[type='radio'] {
|
|
211
|
+
@apply rounded-full;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* Label styling */
|
|
215
|
+
label {
|
|
216
|
+
@apply block text-sm font-medium text-gray-700 mb-1;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* Field wrapper styling */
|
|
220
|
+
.field {
|
|
221
|
+
@apply mb-4;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* Error state styling */
|
|
225
|
+
input.error,
|
|
226
|
+
textarea.error,
|
|
227
|
+
select.error {
|
|
228
|
+
@apply ring-red-500 focus:ring-red-500;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.field-error {
|
|
232
|
+
@apply text-sm text-red-600 mt-1;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* Button styling */
|
|
236
|
+
.btn {
|
|
237
|
+
@apply inline-flex items-center justify-center px-6 py-3 text-base font-semibold rounded-md transition-colors duration-200;
|
|
238
|
+
@apply focus:outline-none focus:ring-2 focus:ring-offset-2;
|
|
239
|
+
@apply disabled:opacity-50 disabled:cursor-not-allowed;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.btn-primary {
|
|
243
|
+
@apply bg-mid text-white hover:opacity-90 focus:ring-mid;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.btn-secondary {
|
|
247
|
+
@apply bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.btn-danger {
|
|
251
|
+
@apply bg-red-600 text-white hover:bg-red-700 focus:ring-red-500;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
63
255
|
/* EditorJS content styles */
|
|
64
256
|
@layer components {
|
|
65
257
|
.codex-editor__redactor .ce-block .ce-block__content {
|
|
@@ -103,7 +295,7 @@
|
|
|
103
295
|
}
|
|
104
296
|
|
|
105
297
|
.cdx-quote__text {
|
|
106
|
-
quotes:
|
|
298
|
+
quotes: '\201C' '\201D' '\2018' '\2019';
|
|
107
299
|
@apply pl-6;
|
|
108
300
|
|
|
109
301
|
&: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,110 @@
|
|
|
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
|
+
# 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
|
+
#
|
|
11
|
+
# @example Basic button
|
|
12
|
+
# render Panda::Core::UI::Button.new(text: "Click me")
|
|
13
|
+
#
|
|
14
|
+
# @example Button as link
|
|
15
|
+
# render Panda::Core::UI::Button.new(
|
|
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"
|
|
26
|
+
# )
|
|
27
|
+
#
|
|
28
|
+
# @example With custom attributes
|
|
29
|
+
# render Panda::Core::UI::Button.new(
|
|
30
|
+
# text: "Submit",
|
|
31
|
+
# variant: :primary,
|
|
32
|
+
# class: "mt-4",
|
|
33
|
+
# data: { turbo_method: :post }
|
|
34
|
+
# )
|
|
35
|
+
#
|
|
36
|
+
class Button < Panda::Core::Base
|
|
37
|
+
# Type-safe properties using Literal
|
|
38
|
+
prop :text, String
|
|
39
|
+
prop :variant, Symbol, default: :default
|
|
40
|
+
prop :size, Symbol, default: :medium
|
|
41
|
+
prop :disabled, _Boolean, default: false
|
|
42
|
+
prop :type, String, default: "button"
|
|
43
|
+
prop :href, _Nilable(String), default: -> {}
|
|
44
|
+
|
|
45
|
+
def view_template
|
|
46
|
+
if @href
|
|
47
|
+
a(**@attrs) { @text }
|
|
48
|
+
else
|
|
49
|
+
button(**@attrs) { @text }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def default_attrs
|
|
54
|
+
base = {
|
|
55
|
+
class: button_classes
|
|
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
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def button_classes
|
|
71
|
+
base = "inline-flex items-center rounded-md font-semibold"
|
|
72
|
+
base += " #{size_classes}"
|
|
73
|
+
base += " #{variant_classes}"
|
|
74
|
+
base += " disabled:opacity-50 disabled:cursor-not-allowed" if @disabled
|
|
75
|
+
base
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def size_classes
|
|
79
|
+
case @size
|
|
80
|
+
when :small, :sm
|
|
81
|
+
"px-2.5 py-1.5 text-sm"
|
|
82
|
+
when :large, :lg
|
|
83
|
+
"px-3.5 py-2.5 text-lg"
|
|
84
|
+
else # :medium, :md
|
|
85
|
+
"px-3 py-2 text-sm"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def variant_classes
|
|
90
|
+
case @variant
|
|
91
|
+
when :primary
|
|
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"
|
|
94
|
+
when :secondary
|
|
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"
|
|
97
|
+
when :success
|
|
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"
|
|
99
|
+
when :danger
|
|
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"
|
|
101
|
+
when :ghost
|
|
102
|
+
"bg-transparent text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
|
103
|
+
else # :default
|
|
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"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
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
|